diff --git a/src/desktop/chat/chatbox.cpp b/src/desktop/chat/chatbox.cpp index e067138013..72584b7154 100644 --- a/src/desktop/chat/chatbox.cpp +++ b/src/desktop/chat/chatbox.cpp @@ -1,24 +1,21 @@ // SPDX-License-Identifier: GPL-3.0-or-later - #include "desktop/chat/chatbox.h" #include "desktop/chat/chatwidget.h" #include "desktop/chat/chatwindow.h" #include "desktop/chat/useritemdelegate.h" -#include "desktop/widgets/groupedtoolbutton.h" #include "desktop/utils/widgetutils.h" -#include "libclient/document.h" +#include "desktop/widgets/groupedtoolbutton.h" #include "libclient/canvas/canvasmodel.h" #include "libclient/canvas/userlist.h" -#include "libclient/drawdance/message.h" +#include "libclient/document.h" #include "libclient/net/client.h" - #include -#include -#include #include -#include #include #include +#include +#include +#include namespace widgets { @@ -48,12 +45,14 @@ ChatBox::ChatBox(Document *doc, QWidget *parent) m_inviteButton->setText(tr("Invite")); buttonsLayout->addWidget(m_inviteButton); - m_sessionSettingsButton = new GroupedToolButton{GroupedToolButton::GroupCenter, this}; + m_sessionSettingsButton = + new GroupedToolButton{GroupedToolButton::GroupCenter, this}; m_sessionSettingsButton->setIcon(QIcon::fromTheme("configure")); m_sessionSettingsButton->setText(tr("Session")); buttonsLayout->addWidget(m_sessionSettingsButton); - m_chatMenuButton = new GroupedToolButton{GroupedToolButton::GroupRight, this}; + m_chatMenuButton = + new GroupedToolButton{GroupedToolButton::GroupRight, this}; m_chatMenuButton->setIcon(QIcon::fromTheme("edit-comment")); m_chatMenuButton->setText(tr("Chat")); m_chatMenuButton->setStatusTip(tr("Show chat options")); @@ -83,25 +82,42 @@ ChatBox::ChatBox(Document *doc, QWidget *parent) setLayout(layout); connect(m_chatWidget, &ChatWidget::message, this, &ChatBox::message); - connect(m_chatWidget, &ChatWidget::detachRequested, this, &ChatBox::detachFromParent); + connect( + m_chatWidget, &ChatWidget::detachRequested, this, + &ChatBox::detachFromParent); connect(m_chatWidget, &ChatWidget::expandRequested, this, [this]() { if(isCollapsed()) { emit expandPlease(); } }); - connect(m_chatWidget, &ChatWidget::muteChanged, this, &ChatBox::muteChanged); + connect( + m_chatWidget, &ChatWidget::muteChanged, this, &ChatBox::muteChanged); connect(doc, &Document::canvasChanged, this, &ChatBox::onCanvasChanged); connect(doc, &Document::serverLoggedIn, this, &ChatBox::onServerLogin); - connect(doc, &Document::compatibilityModeChanged, this, &ChatBox::onCompatibilityModeChanged); - - connect(doc, &Document::sessionPreserveChatChanged, m_chatWidget, &ChatWidget::setPreserveMode); - connect(doc->client(), &net::Client::serverMessage, m_chatWidget, &ChatWidget::systemMessage); - connect(doc->client(), &net::Client::youWereKicked, m_chatWidget, &ChatWidget::kicked); - - connect(m_userItemDelegate, &widgets::UserItemDelegate::opCommand, doc->client(), &net::Client::sendMessage); - connect(m_userItemDelegate, &widgets::UserItemDelegate::requestPrivateChat, m_chatWidget, &ChatWidget::openPrivateChat); - connect(m_userItemDelegate, &widgets::UserItemDelegate::requestUserInfo, this, &ChatBox::requestUserInfo); + connect( + doc, &Document::compatibilityModeChanged, this, + &ChatBox::onCompatibilityModeChanged); + + connect( + doc, &Document::sessionPreserveChatChanged, m_chatWidget, + &ChatWidget::setPreserveMode); + connect( + doc->client(), &net::Client::serverMessage, m_chatWidget, + &ChatWidget::systemMessage); + connect( + doc->client(), &net::Client::youWereKicked, m_chatWidget, + &ChatWidget::kicked); + + connect( + m_userItemDelegate, &widgets::UserItemDelegate::opCommand, + doc->client(), &net::Client::sendMessage); + connect( + m_userItemDelegate, &widgets::UserItemDelegate::requestPrivateChat, + m_chatWidget, &ChatWidget::openPrivateChat); + connect( + m_userItemDelegate, &widgets::UserItemDelegate::requestUserInfo, this, + &ChatBox::requestUserInfo); } void ChatBox::setActions(QAction *inviteAction, QAction *sessionSettingsAction) @@ -116,7 +132,7 @@ void ChatBox::setActions(QAction *inviteAction, QAction *sessionSettingsAction) #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) connect(action, &QAction::enabledChanged, button, &QWidget::setEnabled); #else - connect(action, &QAction::changed, this, [=]{ + connect(action, &QAction::changed, this, [=] { button->setEnabled(action->isEnabled()); }); #endif @@ -133,15 +149,23 @@ void ChatBox::onCanvasChanged(canvas::CanvasModel *canvas) m_userList->setModel(canvas->userlist()->onlineUsers()); m_chatWidget->setUserList(canvas->userlist()); - connect(canvas, &canvas::CanvasModel::chatMessageReceived, m_chatWidget, &ChatWidget::receiveMessage); - connect(canvas, &canvas::CanvasModel::pinnedMessageChanged, m_chatWidget, &ChatWidget::setPinnedMessage); - connect(canvas, &canvas::CanvasModel::userJoined, m_chatWidget, &ChatWidget::userJoined); - connect(canvas, &canvas::CanvasModel::userLeft, m_chatWidget, &ChatWidget::userParted); + connect( + canvas, &canvas::CanvasModel::chatMessageReceived, m_chatWidget, + &ChatWidget::receiveMessage); + connect( + canvas, &canvas::CanvasModel::pinnedMessageChanged, m_chatWidget, + &ChatWidget::setPinnedMessage); + connect( + canvas, &canvas::CanvasModel::userJoined, m_chatWidget, + &ChatWidget::userJoined); + connect( + canvas, &canvas::CanvasModel::userLeft, m_chatWidget, + &ChatWidget::userParted); } void ChatBox::onServerLogin() { - m_chatWidget->loggedIn(static_cast(sender())->client()->myId()); + m_chatWidget->loggedIn(static_cast(sender())->client()->myId()); } void ChatBox::onCompatibilityModeChanged(bool compatibilityMode) @@ -156,7 +180,7 @@ void ChatBox::focusInput() void ChatBox::detachFromParent() { - if(!parent() || qobject_cast(parent())) + if(!parent() || qobject_cast(parent())) return; m_state = State::Detached; diff --git a/src/desktop/chat/chatbox.h b/src/desktop/chat/chatbox.h index d5e35361bc..81d52c4d5e 100644 --- a/src/desktop/chat/chatbox.h +++ b/src/desktop/chat/chatbox.h @@ -1,23 +1,20 @@ // SPDX-License-Identifier: GPL-3.0-or-later - #ifndef CHATBOX_H #define CHATBOX_H - #include +class Document; +class QListView; +class QPushButton; + namespace canvas { - class CanvasModel; +class CanvasModel; } -namespace drawdance { - class Message; +namespace net { +class Message; } -class QListView; -class QPushButton; - -class Document; - namespace widgets { class ChatWidget; @@ -27,18 +24,20 @@ class UserItemDelegate; /** * Chat box with user list */ -class ChatBox final : public QWidget -{ +class ChatBox final : public QWidget { Q_OBJECT public: - explicit ChatBox(Document *doc, QWidget *parent=nullptr); + explicit ChatBox(Document *doc, QWidget *parent = nullptr); void setActions(QAction *inviteAction, QAction *sessionSettingsAction); //! Focus the text input widget void focusInput(); - bool isCollapsed() const { return m_state == State::Collapsed || !isVisible(); } + bool isCollapsed() const + { + return m_state == State::Collapsed || !isVisible(); + } private slots: void onCanvasChanged(canvas::CanvasModel *canvas); @@ -49,7 +48,7 @@ private slots: signals: //! User has written a new message - void message(const drawdance::Message &msg); + void message(const net::Message &msg); //! Request information dialog about this user void requestUserInfo(int userId); @@ -60,7 +59,8 @@ private slots: //! Request that the chatbox be expanded void expandPlease(); - //! Detached chat box should be re-attached and reparented (or it will be destroyed) + //! Detached chat box should be re-attached and reparented (or it will be + //! destroyed) void reattachNowPlease(); void muteChanged(bool muted); @@ -69,11 +69,7 @@ private slots: void resizeEvent(QResizeEvent *event) override; private: - enum class State { - Expanded, - Collapsed, - Detached - }; + enum class State { Expanded, Collapsed, Detached }; ChatWidget *m_chatWidget; UserItemDelegate *m_userItemDelegate; @@ -88,4 +84,3 @@ private slots: } #endif - diff --git a/src/desktop/chat/chatwidget.cpp b/src/desktop/chat/chatwidget.cpp index e17466f1eb..6ce30e05d4 100644 --- a/src/desktop/chat/chatwidget.cpp +++ b/src/desktop/chat/chatwidget.cpp @@ -1,29 +1,25 @@ // SPDX-License-Identifier: GPL-3.0-or-later - -#include "desktop/chat/chatwidgetpinnedarea.h" #include "desktop/chat/chatwidget.h" -#include "libclient/utils/html.h" -#include "libclient/utils/funstuff.h" +#include "desktop/chat/chatwidgetpinnedarea.h" #include "desktop/main.h" #include "desktop/utils/widgetutils.h" - #include "libclient/canvas/userlist.h" -#include "libclient/drawdance/message.h" #include "libclient/drawdance/perf.h" - -#include -#include -#include -#include +#include "libclient/net/message.h" +#include "libclient/utils/funstuff.h" +#include "libclient/utils/html.h" #include -#include -#include -#include #include +#include #include #include +#include +#include #include - +#include +#include +#include +#include #ifdef Q_OS_ANDROID # include "desktop/chat/chatlineeditandroid.h" #else @@ -40,44 +36,52 @@ struct Chat { qint64 lastMessageTs = 0; int scrollPosition = 0; - Chat() : doc(nullptr) { } + Chat() + : doc(nullptr) + { + } explicit Chat(QObject *parent) : doc(new QTextDocument(parent)) { - doc->setDefaultStyleSheet( - ".sep { background: #4d4d4d }" - ".notification { background: #232629 }" - ".message, .notification {" - "color: #eff0f1;" - "margin: 1px 0 1px 0" - "}" - ".alert { background: #66da4453 }" - ".shout { background: #34292c }" - ".shout .tab { background: #da4453 }" - ".action { font-style: italic }" - ".username { font-weight: bold }" - ".trusted { color: #27ae60 }" - ".registered { color: #16a085 }" - ".op { color: #f47750 }" - ".mod { color: #ed1515 }" - ".timestamp { color: #8d8d8d }" - ".alert .timestamp { color: #eff0f1 }" - "a:link { color: #1d99f3 }" - ".emoji { font-size: xx-large; }" - ); + doc->setDefaultStyleSheet(".sep { background: #4d4d4d }" + ".notification { background: #232629 }" + ".message, .notification {" + "color: #eff0f1;" + "margin: 1px 0 1px 0" + "}" + ".alert { background: #66da4453 }" + ".shout { background: #34292c }" + ".shout .tab { background: #da4453 }" + ".action { font-style: italic }" + ".username { font-weight: bold }" + ".trusted { color: #27ae60 }" + ".registered { color: #16a085 }" + ".op { color: #f47750 }" + ".mod { color: #ed1515 }" + ".timestamp { color: #8d8d8d }" + ".alert .timestamp { color: #eff0f1 }" + "a:link { color: #1d99f3 }" + ".emoji { font-size: xx-large; }"); } void appendSeparator(QTextCursor &cursor); - void appendMessage(int userId, const QString &usernameSpan, const QString &message, bool shout, bool alert); - void appendMessageCompact(int userId, const QString &usernameSpan, const QString &message, bool shout, bool alert); + void appendMessage( + int userId, const QString &usernameSpan, const QString &message, + bool shout, bool alert); + void appendMessageCompact( + int userId, const QString &usernameSpan, const QString &message, + bool shout, bool alert); void appendAction(const QString &usernameSpan, const QString &message); void appendNotification(const QString &message); }; struct ChatWidget::Private { - Private(ChatWidget *parent) : chatbox(parent) { } + Private(ChatWidget *parent) + : chatbox(parent) + { + } - ChatWidget * const chatbox; + ChatWidget *const chatbox; QTextBrowser *view = nullptr; ChatLineEdit *myline = nullptr; ChatWidgetPinnedArea *pinned = nullptr; @@ -105,7 +109,8 @@ struct ChatWidget::Private { bool isAtEnd() const { - return view->verticalScrollBar()->value() == view->verticalScrollBar()->maximum(); + return view->verticalScrollBar()->value() == + view->verticalScrollBar()->maximum(); } void scrollChatToEnd(int ifCurrentId) @@ -117,7 +122,8 @@ struct ChatWidget::Private { void scrollToEnd() { - view->verticalScrollBar()->setValue(view->verticalScrollBar()->maximum()); + view->verticalScrollBar()->setValue( + view->verticalScrollBar()->maximum()); } inline Chat &publicChat() @@ -132,7 +138,8 @@ struct ChatWidget::Private { }; ChatWidget::ChatWidget(QWidget *parent) - : QWidget(parent), d(new Private(this)) + : QWidget(parent) + , d(new Private(this)) { QVBoxLayout *layout = new QVBoxLayout(this); @@ -158,8 +165,10 @@ ChatWidget::ChatWidget(QWidget *parent) d->tabs->setTabButton(0, QTabBar::RightSide, nullptr); } - connect(d->tabs, &QTabBar::currentChanged, this, &ChatWidget::chatTabSelected); - connect(d->tabs, &QTabBar::tabCloseRequested, this, &ChatWidget::chatTabClosed); + connect( + d->tabs, &QTabBar::currentChanged, this, &ChatWidget::chatTabSelected); + connect( + d->tabs, &QTabBar::tabCloseRequested, this, &ChatWidget::chatTabClosed); layout->addWidget(d->tabs, 0); d->pinned = new ChatWidgetPinnedArea(this); @@ -168,10 +177,14 @@ ChatWidget::ChatWidget(QWidget *parent) d->view = new QTextBrowser(this); d->view->setOpenExternalLinks(true); utils::initKineticScrolling(d->view); - connect(d->view->verticalScrollBar(), &QScrollBar::valueChanged, this, &ChatWidget::scrollBarMoved); + connect( + d->view->verticalScrollBar(), &QScrollBar::valueChanged, this, + &ChatWidget::scrollBarMoved); d->view->setContextMenuPolicy(Qt::CustomContextMenu); - connect(d->view, &QTextBrowser::customContextMenuRequested, this, &ChatWidget::showChatContextMenu); + connect( + d->view, &QTextBrowser::customContextMenuRequested, this, + &ChatWidget::showChatContextMenu); layout->addWidget(d->view, 1); @@ -180,14 +193,16 @@ ChatWidget::ChatWidget(QWidget *parent) setLayout(layout); - connect(d->myline, &ChatLineEdit::messageSent, this, &ChatWidget::sendMessage); + connect( + d->myline, &ChatLineEdit::messageSent, this, &ChatWidget::sendMessage); d->chats[0] = Chat(this); d->view->setDocument(d->chats[0].doc); d->externalMenu = new QMenu{this}; - d->clearAction = d->externalMenu->addAction(tr("Clear"), this, &ChatWidget::clear); + d->clearAction = + d->externalMenu->addAction(tr("Clear"), this, &ChatWidget::clear); if(!COMPACT_ONLY) { d->compactAction = d->externalMenu->addAction( @@ -197,8 +212,8 @@ ChatWidget::ChatWidget(QWidget *parent) } if(ALLOW_DETACH && !dpApp().smallScreenMode()) { - d->attachAction = d->externalMenu->addAction( - tr("Attach"), this, &ChatWidget::attach); + d->attachAction = + d->externalMenu->addAction(tr("Attach"), this, &ChatWidget::attach); d->detachAction = d->externalMenu->addAction( tr("Detach"), this, &ChatWidget::detachRequested); } @@ -208,7 +223,9 @@ ChatWidget::ChatWidget(QWidget *parent) d->muteAction->setStatusTip(tr("Toggle notifications for this window")); d->muteAction->setCheckable(true); - connect(d->externalMenu, &QMenu::aboutToShow, this, &ChatWidget::contextMenuAboutToShow); + connect( + d->externalMenu, &QMenu::aboutToShow, this, + &ChatWidget::contextMenuAboutToShow); setPreserveMode(false); @@ -250,19 +267,16 @@ void ChatWidget::Private::updatePreserveModeUi() myline->setPlaceholderText(placeholder); chatbox->setStyleSheet( - QStringLiteral( - "QTextEdit, QPlainTextEdit, QLineEdit {" - "background-color: #232629;" - "border: none;" - "color: #eff0f1" - "}" - "QPlainTextEdit, QLineEdit {" - "border-top: 1px solid %1;" - "padding: 4px" - "}" - ).arg(color) - ); - + QStringLiteral("QTextEdit, QPlainTextEdit, QLineEdit {" + "background-color: #232629;" + "border: none;" + "color: #eff0f1" + "}" + "QPlainTextEdit, QLineEdit {" + "border-top: 1px solid %1;" + "padding: 4px" + "}") + .arg(color)); } void ChatWidget::setPreserveMode(bool preservechat) { @@ -283,7 +297,7 @@ void ChatWidget::focusInput() void ChatWidget::setUserList(canvas::UserListModel *userlist) { - d->userlist = userlist; + d->userlist = userlist; } QMenu *ChatWidget::externalMenu() @@ -302,9 +316,7 @@ void ChatWidget::clear() for(const auto &u : d->userlist->users()) { chat.doc->addResource( QTextDocument::ImageResource, - QUrl(QStringLiteral("avatar://%1").arg(u.id)), - u.avatar - ); + QUrl(QStringLiteral("avatar://%1").arg(u.id)), u.avatar); } } } @@ -328,13 +340,11 @@ bool ChatWidget::Private::ensurePrivateChatExists(int userId, QObject *parent) chats[userId].doc->addResource( QTextDocument::ImageResource, QUrl(QStringLiteral("avatar://%1").arg(userId)), - userlist->getUserById(userId).avatar - ); + userlist->getUserById(userId).avatar); chats[userId].doc->addResource( QTextDocument::ImageResource, QUrl(QStringLiteral("avatar://%1").arg(myId)), - userlist->getUserById(myId).avatar - ); + userlist->getUserById(myId).avatar); } return true; @@ -345,7 +355,7 @@ void ChatWidget::openPrivateChat(int userId) if(!d->ensurePrivateChatExists(userId, this)) return; - for(int i=d->tabs->count()-1;i>=0;--i) { + for(int i = d->tabs->count() - 1; i >= 0; --i) { if(d->tabs->tabData(i).toInt() == userId) { d->tabs->setCurrentIndex(i); break; @@ -355,14 +365,14 @@ void ChatWidget::openPrivateChat(int userId) static QString timestamp() { - return QStringLiteral("%1").arg( - QDateTime::currentDateTime().toString("HH:mm") - ); + return QStringLiteral("%1") + .arg(QDateTime::currentDateTime().toString("HH:mm")); } QString ChatWidget::Private::usernameSpan(int userId) { - const canvas::User user = userlist ? userlist->getUserById(userId) : canvas::User(); + const canvas::User user = + userlist ? userlist->getUserById(userId) : canvas::User(); QString userclass; if(user.isMod) @@ -374,20 +384,22 @@ QString ChatWidget::Private::usernameSpan(int userId) else if(user.isAuth) userclass = QStringLiteral("registered"); - return QStringLiteral("%2").arg( - userclass, - user.name.isEmpty() ? QStringLiteral("User #%1").arg(userId) : user.name.toHtmlEscaped() - ); + return QStringLiteral("%2") + .arg( + userclass, user.name.isEmpty() + ? QStringLiteral("User #%1").arg(userId) + : user.name.toHtmlEscaped()); } void Chat::appendSeparator(QTextCursor &cursor) { cursor.insertHtml(QStringLiteral( - "
" - )); + "
")); } -void Chat::appendMessageCompact(int userId, const QString &usernameSpan, const QString &message, bool shout, bool alert) +void Chat::appendMessageCompact( + int userId, const QString &usernameSpan, const QString &message, bool shout, + bool alert) { Q_UNUSED(userId); @@ -397,26 +409,27 @@ void Chat::appendMessageCompact(int userId, const QString &usernameSpan, const Q cursor.movePosition(QTextCursor::End); - QString content = - usernameSpan.isEmpty() ? message : QStringLiteral("%1: %2").arg(usernameSpan, message); + QString content = usernameSpan.isEmpty() + ? message + : QStringLiteral("%1: %2").arg(usernameSpan, message); - cursor.insertHtml(QStringLiteral( - "" - "" - "" - "" - "" - "" - "
%2%3
" - ).arg( - alert ? " alert" : shout ? " shout" : "", - htmlutils::newlineToBr(content), - timestamp() - ) - ); -} - -void Chat::appendMessage(int userId, const QString &usernameSpan, const QString &message, bool shout, bool alert) + cursor.insertHtml(QStringLiteral("" + "" + "" + "" + "" + "" + "
%2%3
") + .arg( + alert ? " alert" + : shout ? " shout" + : "", + htmlutils::newlineToBr(content), timestamp())); +} + +void Chat::appendMessage( + int userId, const QString &usernameSpan, const QString &message, bool shout, + bool alert) { QTextCursor cursor(doc); cursor.movePosition(QTextCursor::End); @@ -436,8 +449,9 @@ void Chat::appendMessage(int userId, const QString &usernameSpan, const QString cursor.insertHtml(QStringLiteral("
")); - // Using css property "white-space: pre" only works for the first message. Newlines disappear on subsequent messages. - // Thus the need to manually replace newlines by
+ // Using css property "white-space: pre" only works for the first + // message. Newlines disappear on subsequent messages. Thus the need to + // manually replace newlines by
cursor.insertHtml(htmlutils::newlineToBr(message)); return; @@ -447,26 +461,24 @@ void Chat::appendMessage(int userId, const QString &usernameSpan, const QString // http://doc.qt.io/qt-5/richtext-html-subset.html // Embedding a whole browser engine just to render the chat widget would // be excessive. - cursor.insertHtml(QStringLiteral( - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "
%3%4
%5
" - ).arg( - alert ? " alert" : shout ? " shout" : "", - QString::number(userId), - usernameSpan, - timestamp(), - htmlutils::newlineToBr(message) - ) - ); + cursor.insertHtml( + QStringLiteral("" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "
%3%4
%5
") + .arg( + alert ? " alert" + : shout ? " shout" + : "", + QString::number(userId), usernameSpan, timestamp(), + htmlutils::newlineToBr(message))); lastMessageTs = ts; } @@ -479,20 +491,14 @@ void Chat::appendAction(const QString &usernameSpan, const QString &message) appendSeparator(cursor); lastAppendedId = -1; } - cursor.insertHtml(QStringLiteral( - "" - "" - "" - "" - "" - "" - "
%1 %2%3
" - ).arg( - usernameSpan, - message, - timestamp() - ) - ); + cursor.insertHtml(QStringLiteral("" + "" + "" + "" + "" + "" + "
%1 %2%3
") + .arg(usernameSpan, message, timestamp())); } void Chat::appendNotification(const QString &message) @@ -505,17 +511,13 @@ void Chat::appendNotification(const QString &message) lastAppendedId = 0; } - cursor.insertHtml(QStringLiteral( - "" - "" - "" - "" - "
%1%2
" - ).arg( - htmlutils::newlineToBr(message), - timestamp() - ) - ); + cursor.insertHtml( + QStringLiteral("" + "" + "" + "" + "
%1%2
") + .arg(htmlutils::newlineToBr(message), timestamp())); } void ChatWidget::userJoined(int id, const QString &name) @@ -526,18 +528,19 @@ void ChatWidget::userJoined(int id, const QString &name) d->chats[0].doc->addResource( QTextDocument::ImageResource, QUrl(QStringLiteral("avatar://%1").arg(id)), - d->userlist->getUserById(id).avatar - ); + d->userlist->getUserById(id).avatar); if(d->chats.contains(id)) { d->chats[id].doc->addResource( QTextDocument::ImageResource, QUrl(QStringLiteral("avatar://%1").arg(id)), - d->userlist->getUserById(id).avatar - ); + d->userlist->getUserById(id).avatar); } } else { - qWarning("User #%d logged in, but userlist object not assigned to ChatWidget!", id); + qWarning( + "User #%d logged in, but userlist object not assigned to " + "ChatWidget!", + id); } // The server resends UserJoin messages during session reset. @@ -585,19 +588,24 @@ void ChatWidget::userParted(int id) void ChatWidget::kicked(const QString &kickedBy) { const bool wasAtEnd = d->isAtEnd(); - d->publicChat().appendNotification(tr("You have been kicked by %1").arg(kickedBy.toHtmlEscaped())); + d->publicChat().appendNotification( + tr("You have been kicked by %1").arg(kickedBy.toHtmlEscaped())); if(wasAtEnd) d->scrollChatToEnd(0); } -void ChatWidget::receiveMessage(int sender, int recipient, uint8_t tflags, uint8_t oflags, const QString &message) +void ChatWidget::receiveMessage( + int sender, int recipient, uint8_t tflags, uint8_t oflags, + const QString &message) { Q_UNUSED(tflags); const bool wasAtEnd = d->isAtEnd(); - // The server echoes our PMs back to us, in which case we identify the chat box - // the message belongs to based on the recipient field rather than the sender (which is us) - const int chatId = recipient > 0 ? (recipient == d->myId ? sender : recipient) : 0; + // The server echoes our PMs back to us, in which case we identify the chat + // box the message belongs to based on the recipient field rather than the + // sender (which is us) + const int chatId = + recipient > 0 ? (recipient == d->myId ? sender : recipient) : 0; if(chatId > 0) { if(!d->ensurePrivateChatExists(chatId, this)) @@ -616,13 +624,17 @@ void ChatWidget::receiveMessage(int sender, int recipient, uint8_t tflags, uint8 if(oflags & DP_MSG_CHAT_OFLAGS_ACTION) { chat.appendAction(d->usernameSpan(sender), safetext); } else if(d->compactMode) { - chat.appendMessageCompact(sender, d->usernameSpan(sender), safetext, isValidShout, isValidAlert); + chat.appendMessageCompact( + sender, d->usernameSpan(sender), safetext, isValidShout, + isValidAlert); } else { - chat.appendMessage(sender, d->usernameSpan(sender), safetext, isValidShout, isValidAlert); + chat.appendMessage( + sender, d->usernameSpan(sender), safetext, isValidShout, + isValidAlert); } if(isValidAlert) { - for(int i=0;itabs->count();++i) { + for(int i = 0; i < d->tabs->count(); ++i) { if(d->tabs->tabData(i).toInt() == chatId) { d->tabs->setCurrentIndex(i); break; @@ -630,7 +642,7 @@ void ChatWidget::receiveMessage(int sender, int recipient, uint8_t tflags, uint8 } emit expandRequested(); } else if(chatId != d->currentChat) { - for(int i=0;itabs->count();++i) { + for(int i = 0; i < d->tabs->count(); ++i) { if(d->tabs->tabData(i).toInt() == chatId) { d->tabs->setTabTextColor(i, QColor(218, 68, 83)); break; @@ -655,13 +667,14 @@ void ChatWidget::setPinnedMessage(const QString &message) } } -void ChatWidget::systemMessage(const QString& message, bool alert) +void ChatWidget::systemMessage(const QString &message, bool alert) { const bool wasAtEnd = d->isAtEnd(); const QString safetext = htmlutils::linkify(htmlutils::emojify(message.toHtmlEscaped())); if(alert) { - d->publicChat().appendMessageCompact(0, QString(), safetext, false, true); + d->publicChat().appendMessageCompact( + 0, QString(), safetext, false, true); emit expandRequested(); } else { d->publicChat().appendNotification(safetext); @@ -691,21 +704,22 @@ void ChatWidget::sendMessage(QString chatMessage) // Special commands int split = chatMessage.indexOf(' '); - if(split<0) + if(split < 0) split = chatMessage.length(); - const auto cmd = chatMessage.mid(1, split-1); + const auto cmd = chatMessage.mid(1, split - 1); const auto params = chatMessage.mid(split).trimmed(); if(cmd == QStringLiteral("clear")) { clear(); return; - } else if(cmd.at(0)=='!' && d->currentChat == 0) { + } else if(cmd.at(0) == '!' && d->currentChat == 0) { if(!d->userlist || !d->userlist->isOperator(d->myId)) { - systemMessage(tr("/!: only operators are allowed to send shouts.")); + systemMessage( + tr("/!: only operators are allowed to send shouts.")); return; - } else if (d->currentChat != 0) { + } else if(d->currentChat != 0) { systemMessage(tr("/!: can only shout in a public chat.")); return; } else if(chatMessage.length() <= 2) { @@ -718,7 +732,8 @@ void ChatWidget::sendMessage(QString chatMessage) } else if(cmd == QStringLiteral("alert")) { if(!d->userlist || !d->userlist->isOperator(d->myId)) { - systemMessage(tr("/alert: only operators are allowed to send alerts.")); + systemMessage( + tr("/alert: only operators are allowed to send alerts.")); return; } else if(params.isEmpty()) { systemMessage(tr("/alert: no text given.")); @@ -741,7 +756,7 @@ void ChatWidget::sendMessage(QString chatMessage) if(!d->userlist || !d->userlist->isOperator(d->myId)) { systemMessage(tr("/pin: only operators are allowed to pin.")); return; - } else if (d->currentChat != 0) { + } else if(d->currentChat != 0) { systemMessage(tr("/pin: can only pin in a public chat.")); return; } else if(params.isEmpty()) { @@ -755,9 +770,10 @@ void ChatWidget::sendMessage(QString chatMessage) } else if(cmd == QStringLiteral("unpin")) { if(!d->userlist || !d->userlist->isOperator(d->myId)) { - systemMessage(tr("/unpin: only operators are allowed to unpin.")); + systemMessage( + tr("/unpin: only operators are allowed to unpin.")); return; - } else if (d->currentChat != 0) { + } else if(d->currentChat != 0) { systemMessage(tr("/unpin: can only unpin in a public chat.")); return; } else { @@ -768,8 +784,9 @@ void ChatWidget::sendMessage(QString chatMessage) } else if(cmd == QStringLiteral("roll")) { // TODO this should be done serverside to prevent cheating - utils::DiceRoll result = utils::diceRoll(params.isEmpty() ? QStringLiteral("1d6") : params); - if(result.number>0) { + utils::DiceRoll result = utils::diceRoll( + params.isEmpty() ? QStringLiteral("1d6") : params); + if(result.number > 0) { oflags = DP_MSG_CHAT_OFLAGS_ACTION; effectiveMessage = "rolls " + result.toString(); } else { @@ -785,11 +802,11 @@ void ChatWidget::sendMessage(QString chatMessage) "/clear - clear chat window\n" "/! - make an announcement (Operators only)\n" "/alert - send a high priority alert (Operators only)\n" - "/pin - pin a message to the top of the chat box (Operators only)\n" + "/pin - pin a message to the top of the chat box " + "(Operators only)\n" "/unpin - remove pinned message (Operators only)\n" "/me - send action-type message\n" - "/roll - roll dice, e.g. 1d6 for a six-sided die" - ); + "/roll - roll dice, e.g. 1d6 for a six-sided die"); systemMessage(text); return; @@ -802,15 +819,18 @@ void ChatWidget::sendMessage(QString chatMessage) // Send the chat message int target = d->currentChat; uint8_t contextId = d->myId; - drawdance::Message msg = target == 0 - ? drawdance::Message::makeChat(contextId, tflags, oflags, effectiveMessage) - : drawdance::Message::makePrivateChat(contextId, target, oflags, effectiveMessage); + net::Message msg = + target == 0 + ? net::makeChatMessage(contextId, tflags, oflags, effectiveMessage) + : net::makePrivateChatMessage( + contextId, target, oflags, effectiveMessage); emit message(msg); } void ChatWidget::chatTabSelected(int index) { - d->chats[d->currentChat].scrollPosition = d->view->verticalScrollBar()->value(); + d->chats[d->currentChat].scrollPosition = + d->view->verticalScrollBar()->value(); const int id = d->tabs->tabData(index).toInt(); Q_ASSERT(d->chats.contains(id)); diff --git a/src/desktop/chat/chatwidget.h b/src/desktop/chat/chatwidget.h index a48956cfdb..d9b81a765e 100644 --- a/src/desktop/chat/chatwidget.h +++ b/src/desktop/chat/chatwidget.h @@ -1,17 +1,17 @@ // SPDX-License-Identifier: GPL-3.0-or-later - #ifndef CHATWIDGET_H #define CHATWIDGET_H - #include "desktop/notifications.h" #include class QMenu; -namespace canvas { class UserListModel; } +namespace canvas { +class UserListModel; +} -namespace drawdance { - class Message; +namespace net { +class Message; } namespace widgets { @@ -21,11 +21,10 @@ namespace widgets { * * A widget for chatting with other users */ -class ChatWidget final : public QWidget -{ +class ChatWidget final : public QWidget { Q_OBJECT public: - explicit ChatWidget(QWidget *parent=nullptr); + explicit ChatWidget(QWidget *parent = nullptr); ~ChatWidget() override; void focusInput(); @@ -38,17 +37,19 @@ public slots: /** * @brief Set default message preservation mode * - * This sets a visual cue that informs the user whether chat messages are preserved - * in the session history or not + * This sets a visual cue that informs the user whether chat messages are + * preserved in the session history or not * */ void setPreserveMode(bool preservechat); //! Display a received message - void receiveMessage(int sender, int recipient, uint8_t tflags, uint8_t oflags, const QString &message); + void receiveMessage( + int sender, int recipient, uint8_t tflags, uint8_t oflags, + const QString &message); //! Display a system message - void systemMessage(const QString& message, bool isAlert=false); + void systemMessage(const QString &message, bool isAlert = false); //! Set the message pinned to the top of the chat box void setPinnedMessage(const QString &message); @@ -80,7 +81,7 @@ private slots: void attach(); signals: - void message(const drawdance::Message &msg); + void message(const net::Message &msg); void detachRequested(); void expandRequested(); void muteChanged(bool muted); @@ -106,4 +107,3 @@ private slots: } #endif - diff --git a/src/desktop/chat/useritemdelegate.cpp b/src/desktop/chat/useritemdelegate.cpp index 6c0c47dfd6..8958a26d0c 100644 --- a/src/desktop/chat/useritemdelegate.cpp +++ b/src/desktop/chat/useritemdelegate.cpp @@ -1,19 +1,17 @@ // SPDX-License-Identifier: GPL-3.0-or-later - #include "desktop/chat/useritemdelegate.h" +#include "desktop/utils/qtguicompat.h" #include "libclient/canvas/acl.h" -#include "libclient/canvas/userlist.h" #include "libclient/canvas/canvasmodel.h" -#include "libclient/drawdance/message.h" -#include "libclient/net/servercmd.h" +#include "libclient/canvas/userlist.h" #include "libclient/document.h" -#include "desktop/utils/qtguicompat.h" - -#include +#include "libclient/net/message.h" +#include "libshared/net/servercmd.h" #include +#include #include #include -#include +#include namespace widgets { @@ -24,7 +22,8 @@ static const int STATUS_OVERLAY_SIZE = 16; static const int BUTTON_WIDTH = 16; UserItemDelegate::UserItemDelegate(QObject *parent) - : QAbstractItemDelegate(parent), m_doc(nullptr) + : QAbstractItemDelegate(parent) + , m_doc(nullptr) { m_userMenu = new QMenu; m_menuTitle = m_userMenu->addSection("User"); @@ -53,16 +52,26 @@ UserItemDelegate::UserItemDelegate(QObject *parent) m_lockAction->setCheckable(true); m_muteAction->setCheckable(true); - connect(m_opAction, &QAction::triggered, this, &UserItemDelegate::toggleOpMode); - connect(m_trustAction, &QAction::triggered, this, &UserItemDelegate::toggleTrusted); - connect(m_lockAction, &QAction::triggered, this, &UserItemDelegate::toggleLock); - connect(m_muteAction, &QAction::triggered, this, &UserItemDelegate::toggleMute); - connect(m_kickAction, &QAction::triggered, this, &UserItemDelegate::kickUser); + connect( + m_opAction, &QAction::triggered, this, &UserItemDelegate::toggleOpMode); + connect( + m_trustAction, &QAction::triggered, this, + &UserItemDelegate::toggleTrusted); + connect( + m_lockAction, &QAction::triggered, this, &UserItemDelegate::toggleLock); + connect( + m_muteAction, &QAction::triggered, this, &UserItemDelegate::toggleMute); + connect( + m_kickAction, &QAction::triggered, this, &UserItemDelegate::kickUser); connect(m_banAction, &QAction::triggered, this, &UserItemDelegate::banUser); connect(m_chatAction, &QAction::triggered, this, &UserItemDelegate::pmUser); - connect(m_infoAction, &QAction::triggered, this, &UserItemDelegate::showUserInfo); - connect(m_undoAction, &QAction::triggered, this, &UserItemDelegate::undoByUser); - connect(m_redoAction, &QAction::triggered, this, &UserItemDelegate::redoByUser); + connect( + m_infoAction, &QAction::triggered, this, + &UserItemDelegate::showUserInfo); + connect( + m_undoAction, &QAction::triggered, this, &UserItemDelegate::undoByUser); + connect( + m_redoAction, &QAction::triggered, this, &UserItemDelegate::redoByUser); m_lockIcon = QIcon::fromTheme("object-locked"); m_muteIcon = QIcon::fromTheme("irc-unvoice"); @@ -78,15 +87,18 @@ void UserItemDelegate::setCompatibilityMode(bool compatibilityMode) m_infoAction->setDisabled(compatibilityMode); } -QSize UserItemDelegate::sizeHint(const QStyleOptionViewItem & option, const QModelIndex & index) const +QSize UserItemDelegate::sizeHint( + const QStyleOptionViewItem &option, const QModelIndex &index) const { Q_UNUSED(option); Q_UNUSED(index); - return QSize(AVATAR_SIZE*4, AVATAR_SIZE+2*MARGIN); + return QSize(AVATAR_SIZE * 4, AVATAR_SIZE + 2 * MARGIN); } -void UserItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const +void UserItemDelegate::paint( + QPainter *painter, const QStyleOptionViewItem &option, + const QModelIndex &index) const { painter->save(); painter->fillRect(option.rect, option.backgroundBrush); @@ -94,25 +106,20 @@ void UserItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &opti // Draw avatar const QRect avatarRect( - option.rect.x() + MARGIN, - option.rect.y() + MARGIN, - AVATAR_SIZE, - AVATAR_SIZE - ); + option.rect.x() + MARGIN, option.rect.y() + MARGIN, AVATAR_SIZE, + AVATAR_SIZE); painter->drawPixmap( avatarRect, - index.data(canvas::UserListModel::AvatarRole).value() - ); + index.data(canvas::UserListModel::AvatarRole).value()); // Draw status overlay - const bool isLocked = index.data(canvas::UserListModel::IsLockedRole).toBool(); + const bool isLocked = + index.data(canvas::UserListModel::IsLockedRole).toBool(); if(isLocked || index.data(canvas::UserListModel::IsMutedRole).toBool()) { const QRect statusOverlayRect( avatarRect.right() - STATUS_OVERLAY_SIZE, - avatarRect.bottom() - STATUS_OVERLAY_SIZE, - STATUS_OVERLAY_SIZE, - STATUS_OVERLAY_SIZE - ); + avatarRect.bottom() - STATUS_OVERLAY_SIZE, STATUS_OVERLAY_SIZE, + STATUS_OVERLAY_SIZE); painter->setBrush(option.palette.color(QPalette::AlternateBase)); painter->setPen(QPen(option.palette.color(QPalette::Base), 2)); @@ -125,21 +132,18 @@ void UserItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &opti // Draw username const QRect usernameRect( - avatarRect.right() + PADDING, - avatarRect.y(), - option.rect.width() - avatarRect.right() - PADDING - BUTTON_WIDTH - MARGIN, - option.rect.height() - MARGIN*2 - ); + avatarRect.right() + PADDING, avatarRect.y(), + option.rect.width() - avatarRect.right() - PADDING - BUTTON_WIDTH - + MARGIN, + option.rect.height() - MARGIN * 2); QFont font = option.font; font.setPixelSize(16); font.setWeight(QFont::Light); painter->setPen(option.palette.text().color()); painter->setFont(font); painter->drawText( - usernameRect, - Qt::AlignLeft | Qt::AlignTop, - index.data(canvas::UserListModel::NameRole).toString() - ); + usernameRect, Qt::AlignLeft | Qt::AlignTop, + index.data(canvas::UserListModel::NameRole).toString()); // Draw user flags QString flags; @@ -174,46 +178,44 @@ void UserItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &opti // Draw the context menu buttons const QRect buttonRect( - option.rect.right() - BUTTON_WIDTH - MARGIN, - option.rect.top() + MARGIN, - BUTTON_WIDTH, - option.rect.height() - 2*MARGIN - ); + option.rect.right() - BUTTON_WIDTH - MARGIN, option.rect.top() + MARGIN, + BUTTON_WIDTH, option.rect.height() - 2 * MARGIN); painter->setPen(Qt::NoPen); - //if((option.state & QStyle::State_MouseOver)) - painter->setBrush(option.palette.color(QPalette::WindowText)); - //else - //painter->setBrush(option.palette.color(QPalette::AlternateBase)); + // if((option.state & QStyle::State_MouseOver)) + painter->setBrush(option.palette.color(QPalette::WindowText)); + // else + // painter->setBrush(option.palette.color(QPalette::AlternateBase)); - const int buttonSize = buttonRect.height()/7; - for(int i=0;i<3;++i) { + const int buttonSize = buttonRect.height() / 7; + for(int i = 0; i < 3; ++i) { painter->drawEllipse(QRect( - buttonRect.x() + (buttonRect.width()-buttonSize)/2, - buttonRect.y() + (1+i*2) * buttonSize, - buttonSize, - buttonSize - )); + buttonRect.x() + (buttonRect.width() - buttonSize) / 2, + buttonRect.y() + (1 + i * 2) * buttonSize, buttonSize, buttonSize)); } painter->restore(); } -bool UserItemDelegate::editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index) +bool UserItemDelegate::editorEvent( + QEvent *event, QAbstractItemModel *model, + const QStyleOptionViewItem &option, const QModelIndex &index) { Q_UNUSED(model); if(event->type() == QEvent::MouseButtonPress && m_doc) { - const QMouseEvent *e = static_cast(event); + const QMouseEvent *e = static_cast(event); - if(e->button() == Qt::RightButton || (e->button() == Qt::LeftButton && compat::mousePos(*e).x() > option.rect.right() - MARGIN - BUTTON_WIDTH)) { + if(e->button() == Qt::RightButton || + (e->button() == Qt::LeftButton && + compat::mousePos(*e).x() > + option.rect.right() - MARGIN - BUTTON_WIDTH)) { showContextMenu(index, compat::globalPos(*e)); return true; } - } - else if(event->type() == QEvent::MouseButtonDblClick && m_doc) { + } else if(event->type() == QEvent::MouseButtonDblClick && m_doc) { const int userId = index.data(canvas::UserListModel::IdRole).toInt(); - if(userId>0 && userId != m_doc->canvas()->localUserId()) { + if(userId > 0 && userId != m_doc->canvas()->localUserId()) { emit requestPrivateChat(userId); return true; } @@ -221,21 +223,28 @@ bool UserItemDelegate::editorEvent(QEvent *event, QAbstractItemModel *model, con return false; } -void UserItemDelegate::showContextMenu(const QModelIndex &index, const QPoint &pos) +void UserItemDelegate::showContextMenu( + const QModelIndex &index, const QPoint &pos) { m_menuId = index.data(canvas::UserListModel::IdRole).toInt(); - m_menuTitle->setText(index.data(canvas::UserListModel::NameRole).toString()); + m_menuTitle->setText( + index.data(canvas::UserListModel::NameRole).toString()); const bool amOp = m_doc->canvas()->aclState()->amOperator(); - const bool amDeputy = m_doc->canvas()->aclState()->amTrusted() && m_doc->isSessionDeputies(); + const bool amDeputy = + m_doc->canvas()->aclState()->amTrusted() && m_doc->isSessionDeputies(); const bool isSelf = m_menuId == m_doc->canvas()->localUserId(); const bool isMod = index.data(canvas::UserListModel::IsModRole).toBool(); - m_opAction->setChecked(index.data(canvas::UserListModel::IsOpRole).toBool()); - m_trustAction->setChecked(index.data(canvas::UserListModel::IsTrustedRole).toBool()); - m_lockAction->setChecked(index.data(canvas::UserListModel::IsLockedRole).toBool()); - m_muteAction->setChecked(index.data(canvas::UserListModel::IsMutedRole).toBool()); + m_opAction->setChecked( + index.data(canvas::UserListModel::IsOpRole).toBool()); + m_trustAction->setChecked( + index.data(canvas::UserListModel::IsTrustedRole).toBool()); + m_lockAction->setChecked( + index.data(canvas::UserListModel::IsLockedRole).toBool()); + m_muteAction->setChecked( + index.data(canvas::UserListModel::IsMutedRole).toBool()); // Can't deop self or moderators m_opAction->setEnabled(amOp && !isSelf && !isMod); @@ -248,15 +257,12 @@ void UserItemDelegate::showContextMenu(const QModelIndex &index, const QPoint &p // Deputies can only kick non-trusted users // No-one can kick themselves or moderators - const bool canKick = !isSelf && !isMod && - ( - amOp || - (amDeputy && !( - index.data(canvas::UserListModel::IsOpRole).toBool() || - index.data(canvas::UserListModel::IsTrustedRole).toBool() - ) - ) - ); + const bool canKick = + !isSelf && !isMod && + (amOp || + (amDeputy && + !(index.data(canvas::UserListModel::IsOpRole).toBool() || + index.data(canvas::UserListModel::IsTrustedRole).toBool()))); m_kickAction->setEnabled(canKick); m_banAction->setEnabled(canKick); @@ -268,17 +274,20 @@ void UserItemDelegate::showContextMenu(const QModelIndex &index, const QPoint &p void UserItemDelegate::toggleOpMode(bool op) { - emit opCommand(m_doc->canvas()->userlist()->getOpUserCommand(m_doc->canvas()->localUserId(), m_menuId, op)); + emit opCommand(m_doc->canvas()->userlist()->getOpUserCommand( + m_doc->canvas()->localUserId(), m_menuId, op)); } void UserItemDelegate::toggleTrusted(bool trust) { - emit opCommand(m_doc->canvas()->userlist()->getTrustUserCommand(m_doc->canvas()->localUserId(), m_menuId, trust)); + emit opCommand(m_doc->canvas()->userlist()->getTrustUserCommand( + m_doc->canvas()->localUserId(), m_menuId, trust)); } void UserItemDelegate::toggleLock(bool op) { - emit opCommand(m_doc->canvas()->userlist()->getLockUserCommand(m_doc->canvas()->localUserId(), m_menuId, op)); + emit opCommand(m_doc->canvas()->userlist()->getLockUserCommand( + m_doc->canvas()->localUserId(), m_menuId, op)); } void UserItemDelegate::toggleMute(bool mute) @@ -308,12 +317,14 @@ void UserItemDelegate::showUserInfo() void UserItemDelegate::undoByUser() { - emit opCommand(drawdance::Message::makeUndo(m_doc->canvas()->localUserId(), m_menuId, false)); + emit opCommand( + net::makeUndoMessage(m_doc->canvas()->localUserId(), m_menuId, false)); } void UserItemDelegate::redoByUser() { - emit opCommand(drawdance::Message::makeUndo(m_doc->canvas()->localUserId(), m_menuId, true)); + emit opCommand( + net::makeUndoMessage(m_doc->canvas()->localUserId(), m_menuId, true)); } } diff --git a/src/desktop/chat/useritemdelegate.h b/src/desktop/chat/useritemdelegate.h index 7f84580331..ab14b8cae4 100644 --- a/src/desktop/chat/useritemdelegate.h +++ b/src/desktop/chat/useritemdelegate.h @@ -1,36 +1,37 @@ // SPDX-License-Identifier: GPL-3.0-or-later - #ifndef USERITEMDELEGATE_H #define USERITEMDELEGATE_H - #include -class QMenu; - class Document; +class QMenu; -namespace drawdance { - class Message; +namespace net { +class Message; } namespace widgets { -class UserItemDelegate final : public QAbstractItemDelegate -{ +class UserItemDelegate final : public QAbstractItemDelegate { Q_OBJECT public: - UserItemDelegate(QObject *parent=nullptr); + UserItemDelegate(QObject *parent = nullptr); ~UserItemDelegate() override; - void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; - QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; - bool editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index) override; + void paint( + QPainter *painter, const QStyleOptionViewItem &option, + const QModelIndex &index) const override; + QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) + const override; + bool editorEvent( + QEvent *event, QAbstractItemModel *model, + const QStyleOptionViewItem &option, const QModelIndex &index) override; void setDocument(Document *doc) { m_doc = doc; } void setCompatibilityMode(bool compatibilityMode); signals: - void opCommand(const drawdance::Message &msg); + void opCommand(const net::Message &msg); void requestPrivateChat(int userId); void requestUserInfo(int userId); diff --git a/src/desktop/dialogs/layerproperties.cpp b/src/desktop/dialogs/layerproperties.cpp index e35c503127..e878c9dd13 100644 --- a/src/desktop/dialogs/layerproperties.cpp +++ b/src/desktop/dialogs/layerproperties.cpp @@ -1,31 +1,34 @@ // SPDX-License-Identifier: GPL-3.0-or-later - #include "desktop/dialogs/layerproperties.h" #include "desktop/utils/widgetutils.h" #include "libclient/canvas/blendmodes.h" -#include "libclient/drawdance/message.h" - +#include "libclient/net/message.h" #include "ui_layerproperties.h" - #include - namespace dialogs { LayerProperties::LayerProperties(uint8_t localUser, QWidget *parent) - : QDialog(parent), m_user(localUser), m_compatibilityMode{false}, m_controlsEnabled{true} + : QDialog(parent) + , m_user(localUser) + , m_compatibilityMode{false} + , m_controlsEnabled{true} { - m_ui = new Ui_LayerProperties; - m_ui->setupUi(this); + m_ui = new Ui_LayerProperties; + m_ui->setupUi(this); utils::setWidgetRetainSizeWhenHidden(m_ui->blendMode, true); - connect(m_ui->title, &QLineEdit::returnPressed, this, &QDialog::accept); - connect(m_ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); - connect(m_ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); - connect(m_ui->buttonBox, &QDialogButtonBox::clicked, this, [this](QAbstractButton *b) { - if(m_ui->buttonBox->buttonRole(b) == QDialogButtonBox::ApplyRole) - emitChanges(); - }); + connect(m_ui->title, &QLineEdit::returnPressed, this, &QDialog::accept); + connect( + m_ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect( + m_ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); + connect( + m_ui->buttonBox, &QDialogButtonBox::clicked, this, + [this](QAbstractButton *b) { + if(m_ui->buttonBox->buttonRole(b) == QDialogButtonBox::ApplyRole) + emitChanges(); + }); connect(this, &QDialog::accepted, this, &LayerProperties::emitChanges); } @@ -34,7 +37,8 @@ LayerProperties::~LayerProperties() delete m_ui; } -void LayerProperties::setLayerItem(const canvas::LayerListItem &item, const QString &creator, bool isDefault) +void LayerProperties::setLayerItem( + const canvas::LayerListItem &item, const QString &creator, bool isDefault) { m_item = item; m_wasDefault = isDefault; @@ -46,18 +50,16 @@ void LayerProperties::setLayerItem(const canvas::LayerListItem &item, const QStr m_ui->defaultLayer->setChecked(isDefault); m_ui->createdBy->setText(creator); updateBlendMode( - m_ui->blendMode, item.blend, item.group, item.isolated, m_compatibilityMode); + m_ui->blendMode, item.blend, item.group, item.isolated, + m_compatibilityMode); } -void LayerProperties::setControlsEnabled(bool enabled) { +void LayerProperties::setControlsEnabled(bool enabled) +{ m_controlsEnabled = enabled; QWidget *w[] = { - m_ui->title, - m_ui->opacitySlider, - m_ui->blendMode, - m_ui->censored - }; - for(unsigned int i=0;ititle, m_ui->opacitySlider, m_ui->blendMode, m_ui->censored}; + for(unsigned int i = 0; i < sizeof(w) / sizeof(w[0]); ++i) w[i]->setEnabled(enabled); } @@ -73,7 +75,8 @@ void LayerProperties::setCompatibilityMode(bool compatibilityMode) blendModeData == -1 ? m_item.blend : DP_BlendMode(blendModeData); m_compatibilityMode = compatibilityMode; updateBlendMode( - m_ui->blendMode, mode, m_item.group, m_item.isolated, m_compatibilityMode); + m_ui->blendMode, mode, m_item.group, m_item.isolated, + m_compatibilityMode); } void LayerProperties::updateBlendMode( @@ -129,18 +132,18 @@ QStandardItemModel *LayerProperties::compatibilityLayerBlendModes() void LayerProperties::showEvent(QShowEvent *event) { - QDialog::showEvent(event); - m_ui->title->setFocus(Qt::PopupFocusReason); - m_ui->title->selectAll(); + QDialog::showEvent(event); + m_ui->title->setFocus(Qt::PopupFocusReason); + m_ui->title->selectAll(); } void LayerProperties::emitChanges() { - drawdance::MessageList messages; + net::MessageList messages; QString title = m_ui->title->text(); if(m_item.title != title) { - messages.append(drawdance::Message::makeLayerRetitle(m_user, m_item.id, title)); + messages.append(net::makeLayerRetitleMessage(m_user, m_item.id, title)); } const int oldOpacity = qRound(m_item.opacity * 100.0); @@ -161,36 +164,33 @@ void LayerProperties::emitChanges() isolated = m_item.isolated; } - if( - m_ui->opacitySlider->value() != oldOpacity || - newBlendmode != m_item.blend || - censored != m_item.censored || - isolated != m_item.isolated - ) { - uint8_t flags = - (censored ? DP_MSG_LAYER_ATTRIBUTES_FLAGS_CENSOR : 0) | - (isolated ? DP_MSG_LAYER_ATTRIBUTES_FLAGS_ISOLATED : 0); + if(m_ui->opacitySlider->value() != oldOpacity || + newBlendmode != m_item.blend || censored != m_item.censored || + isolated != m_item.isolated) { + uint8_t flags = (censored ? DP_MSG_LAYER_ATTRIBUTES_FLAGS_CENSOR : 0) | + (isolated ? DP_MSG_LAYER_ATTRIBUTES_FLAGS_ISOLATED : 0); uint8_t opacity = qRound(m_ui->opacitySlider->value() / 100.0 * 255); - messages.append(drawdance::Message::makeLayerAttributes( + messages.append(net::makeLayerAttributesMessage( m_user, m_item.id, 0, flags, opacity, newBlendmode)); - } + } if(m_ui->visible->isChecked() != (!m_item.hidden)) { emit visibilityChanged(m_item.id, m_ui->visible->isChecked()); - } + } bool makeDefault = m_ui->defaultLayer->isChecked(); - if(m_ui->defaultLayer->isEnabled() && makeDefault != m_wasDefault) { - messages.append(drawdance::Message::makeDefaultLayer( - m_user, makeDefault ? m_item.id : 0)); - } + if(m_ui->defaultLayer->isEnabled() && makeDefault != m_wasDefault) { + messages.append( + net::makeDefaultLayerMessage(m_user, makeDefault ? m_item.id : 0)); + } emit layerCommands(messages.count(), messages.constData()); } void LayerProperties::addBlendModesTo(QStandardItemModel *model) { - for(const canvas::blendmode::Named &m : canvas::blendmode::layerModeNames()) { + for(const canvas::blendmode::Named &m : + canvas::blendmode::layerModeNames()) { QStandardItem *item = new QStandardItem{m.name}; item->setData(int(m.mode), Qt::UserRole); model->appendRow(item); @@ -200,12 +200,12 @@ void LayerProperties::addBlendModesTo(QStandardItemModel *model) int LayerProperties::searchBlendModeIndex(QComboBox *combo, DP_BlendMode mode) { int blendModeCount = combo->count(); - for(int i = 0; i < blendModeCount; ++i) { + for(int i = 0; i < blendModeCount; ++i) { if(combo->itemData(i).toInt() == int(mode)) { - return i; - } - } - return -1; + return i; + } + } + return -1; } } diff --git a/src/desktop/dialogs/layerproperties.h b/src/desktop/dialogs/layerproperties.h index 21ba98c715..dff3f7f1c8 100644 --- a/src/desktop/dialogs/layerproperties.h +++ b/src/desktop/dialogs/layerproperties.h @@ -1,33 +1,31 @@ // SPDX-License-Identifier: GPL-3.0-or-later - #ifndef LAYERPROPERTIES_H #define LAYERPROPERTIES_H - #include "libclient/canvas/layerlist.h" - #include class QComboBox; class QStandardItemModel; class Ui_LayerProperties; -namespace drawdance { - class Message; +namespace net { +class Message; } namespace dialogs { -class LayerProperties final : public QDialog -{ -Q_OBJECT +class LayerProperties final : public QDialog { + Q_OBJECT public: explicit LayerProperties(uint8_t localUser, QWidget *parent = nullptr); ~LayerProperties() override; - void setLayerItem(const canvas::LayerListItem &data, const QString &creator, bool isDefault); + void setLayerItem( + const canvas::LayerListItem &data, const QString &creator, + bool isDefault); void setControlsEnabled(bool enabled); void setOpControlsEnabled(bool enabled); - void setCompatibilityMode(bool compatibilityMode); + void setCompatibilityMode(bool compatibilityMode); int layerId() const { return m_item.id; } @@ -40,11 +38,11 @@ Q_OBJECT static QStandardItemModel *compatibilityLayerBlendModes(); signals: - void layerCommands(int count, const drawdance::Message *msgs); + void layerCommands(int count, const net::Message *msgs); void visibilityChanged(int layerId, bool visible); protected: - virtual void showEvent(QShowEvent *event) override; + virtual void showEvent(QShowEvent *event) override; private slots: void emitChanges(); @@ -53,7 +51,7 @@ private slots: static void addBlendModesTo(QStandardItemModel *model); static int searchBlendModeIndex(QComboBox *combo, DP_BlendMode mode); - Ui_LayerProperties *m_ui; + Ui_LayerProperties *m_ui; canvas::LayerListItem m_item; bool m_wasDefault; uint8_t m_user; diff --git a/src/desktop/dialogs/resetdialog.cpp b/src/desktop/dialogs/resetdialog.cpp index 88264200a2..b7d5119a51 100644 --- a/src/desktop/dialogs/resetdialog.cpp +++ b/src/desktop/dialogs/resetdialog.cpp @@ -1,21 +1,18 @@ // SPDX-License-Identifier: GPL-3.0-or-later - #include "desktop/dialogs/resetdialog.h" #include "desktop/main.h" #include "libclient/canvas/paintengine.h" #include "libclient/utils/images.h" #include "libshared/util/qtcompat.h" - #include "ui_resetsession.h" - -#include +#include +#include #include -#include +#include #include +#include +#include #include -#include -#include -#include namespace { @@ -27,21 +24,24 @@ struct ResetPoint { QVector makeResetPoints(const canvas::PaintEngine *pe) { QVector resetPoints; - pe->snapshotQueue().getSnapshotsWith([&](size_t count, drawdance::SnapshotQueue::SnapshotAtFn at) { - resetPoints.reserve(compat::castSize(count + 1)); - for (size_t i = 0; i < count; ++i) { - DP_Snapshot *s = at(i); - resetPoints.append(ResetPoint{ - drawdance::CanvasState::inc(DP_snapshot_canvas_state_noinc(s)), - QPixmap{}, - }); - } - }); + pe->snapshotQueue().getSnapshotsWith( + [&](size_t count, drawdance::SnapshotQueue::SnapshotAtFn at) { + resetPoints.reserve(compat::castSize(count + 1)); + for(size_t i = 0; i < count; ++i) { + DP_Snapshot *s = at(i); + resetPoints.append(ResetPoint{ + drawdance::CanvasState::inc( + DP_snapshot_canvas_state_noinc(s)), + QPixmap{}, + }); + } + }); drawdance::CanvasState currentCanvasState = pe->historyCanvasState(); int lastIndex = resetPoints.count() - 1; // Don't repeat last reset point if the canvas state hasn't changed since. - if(lastIndex < 0 || currentCanvasState.get() != resetPoints[lastIndex].canvasState.get()) { + if(lastIndex < 0 || + currentCanvasState.get() != resetPoints[lastIndex].canvasState.get()) { resetPoints.append(ResetPoint{ currentCanvasState, QPixmap{}, @@ -77,17 +77,14 @@ struct ResetDialog::Private { static void drawCheckerBackground(QImage &image) { const int TS = 16; - const QBrush checker[] = { - QColor(128,128,128), - QColor(Qt::white) - }; + const QBrush checker[] = {QColor(128, 128, 128), QColor(Qt::white)}; QPainter painter(&image); painter.setCompositionMode(QPainter::CompositionMode_DestinationOver); - for(int y=0;yui->setupUi(this); - d->resetButton = d->ui->buttonBox->addButton(tr("Reset Session"), QDialogButtonBox::DestructiveRole); + d->resetButton = d->ui->buttonBox->addButton( + tr("Reset Session"), QDialogButtonBox::DestructiveRole); d->resetButton->setIcon(QIcon::fromTheme("edit-undo")); - connect(d->resetButton, &QPushButton::clicked, this, &ResetDialog::resetSelected); + connect( + d->resetButton, &QPushButton::clicked, this, + &ResetDialog::resetSelected); #ifndef SINGLE_MAIN_WINDOW // If we can't open a new window, this would obliterate the current session. // That's confusing and not terribly useful, so we don't offer this option. - QPushButton *newButton = d->ui->buttonBox->addButton(tr("New"), QDialogButtonBox::ActionRole); + QPushButton *newButton = + d->ui->buttonBox->addButton(tr("New"), QDialogButtonBox::ActionRole); newButton->setIcon(QIcon::fromTheme("document-new")); connect(newButton, &QPushButton::clicked, this, &ResetDialog::newSelected); #endif - QPushButton *openButton = d->ui->buttonBox->addButton(tr("Open..."), QDialogButtonBox::ActionRole); + QPushButton *openButton = d->ui->buttonBox->addButton( + tr("Open..."), QDialogButtonBox::ActionRole); openButton->setIcon(QIcon::fromTheme("document-open")); - connect(openButton, &QPushButton::clicked, this, &ResetDialog::onOpenClicked); + connect( + openButton, &QPushButton::clicked, this, &ResetDialog::onOpenClicked); d->ui->snapshotSlider->setMaximum(d->resetPoints.size()); - connect(d->ui->snapshotSlider, &QSlider::valueChanged, this, &ResetDialog::onSelectionChanged); + connect( + d->ui->snapshotSlider, &QSlider::valueChanged, this, + &ResetDialog::onSelectionChanged); d->updateSelection(); } @@ -159,11 +166,8 @@ void ResetDialog::onSelectionChanged(int pos) void ResetDialog::onOpenClicked() { const QString file = QFileDialog::getOpenFileName( - this, - tr("Open Image"), - dpApp().settings().lastFileOpenPath(), - utils::fileFormatFilter(utils::FileFormatOption::OpenImages) - ); + this, tr("Open Image"), dpApp().settings().lastFileOpenPath(), + utils::fileFormatFilter(utils::FileFormatOption::OpenImages)); if(file.isEmpty()) return; @@ -187,9 +191,9 @@ void ResetDialog::onOpenClicked() } } -drawdance::MessageList ResetDialog::getResetImage() const +net::MessageList ResetDialog::getResetImage() const { - drawdance::MessageList resetImage; + net::MessageList resetImage; d->resetPoints[d->selection].canvasState.toResetImage(resetImage, 0); return resetImage; } diff --git a/src/desktop/dialogs/resetdialog.h b/src/desktop/dialogs/resetdialog.h index 8c6c88f60b..64a406e6c0 100644 --- a/src/desktop/dialogs/resetdialog.h +++ b/src/desktop/dialogs/resetdialog.h @@ -1,31 +1,27 @@ // SPDX-License-Identifier: GPL-3.0-or-later - #ifndef RESETSESSIONDIALOG_H #define RESETSESSIONDIALOG_H - -#include "libclient/drawdance/message.h" - +#include "libclient/net/message.h" #include namespace canvas { - class PaintEngine; +class PaintEngine; } namespace dialogs { -class ResetDialog final : public QDialog -{ +class ResetDialog final : public QDialog { Q_OBJECT public: ResetDialog( - const canvas::PaintEngine *pe, bool compatibilityMode, - QWidget *parent = nullptr); + const canvas::PaintEngine *pe, bool compatibilityMode, + QWidget *parent = nullptr); ~ResetDialog() override; void setCanReset(bool canReset); - drawdance::MessageList getResetImage() const; + net::MessageList getResetImage() const; signals: void resetSelected(); @@ -45,4 +41,3 @@ private slots: } #endif - diff --git a/src/desktop/dialogs/serverlogdialog.cpp b/src/desktop/dialogs/serverlogdialog.cpp index 9505242616..bcb04bd121 100644 --- a/src/desktop/dialogs/serverlogdialog.cpp +++ b/src/desktop/dialogs/serverlogdialog.cpp @@ -1,17 +1,16 @@ // SPDX-License-Identifier: GPL-3.0-or-later - #include "desktop/dialogs/serverlogdialog.h" #include "libclient/canvas/userlist.h" -#include "libclient/drawdance/message.h" -#include "libclient/net/servercmd.h" +#include "libclient/net/message.h" +#include "libshared/net/servercmd.h" #include "ui_serverlog.h" - #include namespace dialogs { ServerLogDialog::ServerLogDialog(QWidget *parent) - : QDialog(parent), m_opMode(false) + : QDialog(parent) + , m_opMode(false) { m_ui = new Ui_ServerLogDialog; m_ui->setupUi(this); @@ -20,7 +19,9 @@ ServerLogDialog::ServerLogDialog(QWidget *parent) m_ui->view->setModel(m_eventlogProxy); m_eventlogProxy->setFilterCaseSensitivity(Qt::CaseInsensitive); - connect(m_ui->filter, &QLineEdit::textChanged, m_eventlogProxy, &QSortFilterProxyModel::setFilterFixedString); + connect( + m_ui->filter, &QLineEdit::textChanged, m_eventlogProxy, + &QSortFilterProxyModel::setFilterFixedString); m_userlistProxy = new QSortFilterProxyModel(this); m_ui->userlistView->setModel(m_userlistProxy); @@ -28,13 +29,25 @@ ServerLogDialog::ServerLogDialog(QWidget *parent) m_userlistProxy->setFilterKeyColumn(0); m_userlistProxy->setFilterRole(Qt::DisplayRole); m_userlistProxy->setFilterCaseSensitivity(Qt::CaseInsensitive); - connect(m_ui->userlistFilter, &QLineEdit::textChanged, m_userlistProxy, &QSortFilterProxyModel::setFilterFixedString); - - connect(m_ui->inspectMode, &QPushButton::toggled, this, &ServerLogDialog::setInspectMode); - connect(m_ui->kickUser, &QPushButton::clicked, this, &ServerLogDialog::kickSelected); - connect(m_ui->banUser, &QPushButton::clicked, this, &ServerLogDialog::banSelected); - connect(m_ui->undoUser, &QPushButton::clicked, this, &ServerLogDialog::undoSelected); - connect(m_ui->redoUser, &QPushButton::clicked, this, &ServerLogDialog::redoSelected); + connect( + m_ui->userlistFilter, &QLineEdit::textChanged, m_userlistProxy, + &QSortFilterProxyModel::setFilterFixedString); + + connect( + m_ui->inspectMode, &QPushButton::toggled, this, + &ServerLogDialog::setInspectMode); + connect( + m_ui->kickUser, &QPushButton::clicked, this, + &ServerLogDialog::kickSelected); + connect( + m_ui->banUser, &QPushButton::clicked, this, + &ServerLogDialog::banSelected); + connect( + m_ui->undoUser, &QPushButton::clicked, this, + &ServerLogDialog::undoSelected); + connect( + m_ui->redoUser, &QPushButton::clicked, this, + &ServerLogDialog::redoSelected); userSelected(QItemSelection()); } @@ -60,10 +73,15 @@ void ServerLogDialog::setUserList(canvas::UserListModel *userlist) m_userlist = userlist; m_userlistProxy->setSourceModel(userlist); - m_ui->userlistView->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); - m_ui->userlistView->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch); + m_ui->userlistView->horizontalHeader()->setSectionResizeMode( + QHeaderView::ResizeToContents); + m_ui->userlistView->horizontalHeader()->setSectionResizeMode( + 0, QHeaderView::Stretch); - connect(m_ui->userlistView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &ServerLogDialog::userSelected); + connect( + m_ui->userlistView->selectionModel(), + &QItemSelectionModel::selectionChanged, this, + &ServerLogDialog::userSelected); } void ServerLogDialog::setOperatorMode(bool op) @@ -78,7 +96,11 @@ void ServerLogDialog::setOperatorMode(bool op) void ServerLogDialog::userSelected(const QItemSelection &selected) { const bool s = m_opMode && !selected.isEmpty(); - m_ui->kickUser->setEnabled(s && selected.indexes().first().data(canvas::UserListModel::IsOnlineRole).toBool()); + m_ui->kickUser->setEnabled( + s && selected.indexes() + .first() + .data(canvas::UserListModel::IsOnlineRole) + .toBool()); m_ui->banUser->setEnabled(s); m_ui->undoUser->setEnabled(s); m_ui->redoUser->setEnabled(s); @@ -91,9 +113,14 @@ uint8_t ServerLogDialog::selectedUserId() const if(!m_ui->userlistView->selectionModel()) return 0; - const auto selection = m_ui->userlistView->selectionModel()->selection().indexes(); + const auto selection = + m_ui->userlistView->selectionModel()->selection().indexes(); - return selection.isEmpty() ? 0 : uint8_t(selection.first().data(canvas::UserListModel::IdRole).toInt()); + return selection.isEmpty() + ? 0 + : uint8_t(selection.first() + .data(canvas::UserListModel::IdRole) + .toInt()); } void ServerLogDialog::setInspectMode(bool inspect) @@ -126,8 +153,9 @@ void ServerLogDialog::undoSelected() { uint8_t user = selectedUserId(); if(user != 0) { - // note: using a context id of 0 here is acceptable since the server will fix it for us - emit opCommand(drawdance::Message::makeUndo(0, user, false)); + // note: using a context id of 0 here is acceptable since the server + // will fix it for us + emit opCommand(net::makeUndoMessage(0, user, false)); } } @@ -135,10 +163,10 @@ void ServerLogDialog::redoSelected() { uint8_t user = selectedUserId(); if(user != 0) { - // note: using a context id of 0 here is acceptable since the server will fix it for us - emit opCommand(drawdance::Message::makeUndo(0, user, true)); + // note: using a context id of 0 here is acceptable since the server + // will fix it for us + emit opCommand(net::makeUndoMessage(0, user, true)); } } } - diff --git a/src/desktop/dialogs/serverlogdialog.h b/src/desktop/dialogs/serverlogdialog.h index a5b60ee595..5ebc558939 100644 --- a/src/desktop/dialogs/serverlogdialog.h +++ b/src/desktop/dialogs/serverlogdialog.h @@ -1,31 +1,27 @@ // SPDX-License-Identifier: GPL-3.0-or-later - #ifndef SERVERLOGDIALOG_H #define SERVERLOGDIALOG_H - #include -class Ui_ServerLogDialog; - -class QSortFilterProxyModel; -class QItemSelection; class QAbstractItemModel; +class QItemSelection; +class QSortFilterProxyModel; +class Ui_ServerLogDialog; namespace canvas { - class UserListModel; +class UserListModel; } -namespace drawdance { - class Message; +namespace net { +class Message; } namespace dialogs { -class ServerLogDialog final : public QDialog -{ +class ServerLogDialog final : public QDialog { Q_OBJECT public: - ServerLogDialog(QWidget *parent=nullptr); + ServerLogDialog(QWidget *parent = nullptr); ~ServerLogDialog() override; void setModel(QAbstractItemModel *model); @@ -37,7 +33,7 @@ public slots: signals: void inspectModeChanged(int contextId); void inspectModeStopped(); - void opCommand(const drawdance::Message &msg); + void opCommand(const net::Message &msg); protected: void hideEvent(QHideEvent *event) override; @@ -66,4 +62,3 @@ private slots: } #endif - diff --git a/src/desktop/dialogs/settingsdialog/network.cpp b/src/desktop/dialogs/settingsdialog/network.cpp index b626b68333..f7b396cb80 100644 --- a/src/desktop/dialogs/settingsdialog/network.cpp +++ b/src/desktop/dialogs/settingsdialog/network.cpp @@ -61,7 +61,7 @@ Network::Network(desktop::settings::Settings &settings, QWidget *parent) form->addRow(tr("Network timeout:"), utils::encapsulate(tr("%1 seconds"), timeout)); auto *messageQueueDrainRate = new KisSliderSpinBox; - messageQueueDrainRate->setRange(0, libclient::settings::maxMessageQueueDrainRate); + messageQueueDrainRate->setRange(0, net::MessageQueue::MAX_SMOOTH_DRAIN_RATE); settings.bindMessageQueueDrainRate(messageQueueDrainRate); form->addRow(tr("Receive delay:"), messageQueueDrainRate); form->addRow(nullptr, utils::note(tr("The higher the value, the smoother strokes from other users come in."), QSizePolicy::Label)); diff --git a/src/desktop/docks/layerlistdock.cpp b/src/desktop/docks/layerlistdock.cpp index 893824f3f4..0b6ac8daf8 100644 --- a/src/desktop/docks/layerlistdock.cpp +++ b/src/desktop/docks/layerlistdock.cpp @@ -1,45 +1,48 @@ // SPDX-License-Identifier: GPL-3.0-or-later - +#include "desktop/docks/layerlistdock.h" +#include "desktop/dialogs/layerproperties.h" +#include "desktop/docks/layeraclmenu.h" +#include "desktop/docks/layerlistdelegate.h" +#include "desktop/docks/titlewidget.h" +#include "desktop/main.h" +#include "desktop/utils/widgetutils.h" #include "desktop/widgets/groupedtoolbutton.h" #include "desktop/widgets/kis_slider_spin_box.h" -#include "desktop/main.h" #include "libclient/canvas/blendmodes.h" -#include "libclient/canvas/layerlist.h" #include "libclient/canvas/canvasmodel.h" +#include "libclient/canvas/layerlist.h" +#include "libclient/canvas/paintengine.h" #include "libclient/canvas/timelinemodel.h" #include "libclient/canvas/userlist.h" -#include "libclient/canvas/paintengine.h" -#include "desktop/docks/layerlistdock.h" -#include "desktop/docks/layerlistdelegate.h" -#include "desktop/docks/layeraclmenu.h" -#include "desktop/docks/titlewidget.h" -#include "desktop/dialogs/layerproperties.h" #include "libclient/utils/changeflags.h" -#include "desktop/utils/widgetutils.h" - -#include +#include #include #include +#include +#include #include #include #include #include -#include -#include -#include #include +#include +#include #include -#include #include #include namespace docks { LayerList::LayerList(QWidget *parent) - : DockBase(tr("Layers"), parent), - m_canvas(nullptr), m_selectedId(0), m_nearestToDeletedId(0), - m_trackId(0), m_frame(-1), - m_noupdate(false), m_updateBlendModeIndex(-1), m_updateOpacity(-1) + : DockBase(tr("Layers"), parent) + , m_canvas(nullptr) + , m_selectedId(0) + , m_nearestToDeletedId(0) + , m_trackId(0) + , m_frame(-1) + , m_noupdate(false) + , m_updateBlendModeIndex(-1) + , m_updateOpacity(-1) { m_debounceTimer = new QTimer{this}; m_debounceTimer->setSingleShot(true); @@ -49,7 +52,8 @@ LayerList::LayerList(QWidget *parent) auto *titlebar = new TitleWidget(this); setTitleBarWidget(titlebar); - m_lockButton = new widgets::GroupedToolButton{widgets::GroupedToolButton::NotGrouped}; + m_lockButton = + new widgets::GroupedToolButton{widgets::GroupedToolButton::NotGrouped}; m_lockButton->setIcon(QIcon::fromTheme("object-locked")); m_lockButton->setCheckable(true); m_lockButton->setPopupMode(QToolButton::InstantPopup); @@ -76,7 +80,8 @@ LayerList::LayerList(QWidget *parent) m_opacitySlider->setRange(0, 100); m_opacitySlider->setPrefix(tr("Opacity: ")); m_opacitySlider->setSuffix(tr("%")); - m_opacitySlider->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + m_opacitySlider->setSizePolicy( + QSizePolicy::Expanding, QSizePolicy::Preferred); m_opacitySlider->setMinimumWidth(24); connect( m_opacitySlider, QOverload::of(&KisSliderSpinBox::valueChanged), @@ -99,21 +104,31 @@ LayerList::LayerList(QWidget *parent) rootLayout->addWidget(m_view); m_contextMenu = new QMenu(this); - connect(m_view, &QTreeView::customContextMenuRequested, this, &LayerList::showContextMenu); + connect( + m_view, &QTreeView::customContextMenuRequested, this, + &LayerList::showContextMenu); // Layer ACL menu m_aclmenu = new LayerAclMenu(this); m_lockButton->setMenu(m_aclmenu); - connect(m_aclmenu, &LayerAclMenu::layerAclChange, this, &LayerList::changeLayerAcl); - connect(m_aclmenu, &LayerAclMenu::layerCensoredChange, this, &LayerList::censorSelected); + connect( + m_aclmenu, &LayerAclMenu::layerAclChange, this, + &LayerList::changeLayerAcl); + connect( + m_aclmenu, &LayerAclMenu::layerCensoredChange, this, + &LayerList::censorSelected); selectionChanged(QItemSelection()); // Custom layer list item delegate LayerListDelegate *del = new LayerListDelegate(this); - connect(del, &LayerListDelegate::toggleVisibility, this, &LayerList::setLayerVisibility); - connect(del, &LayerListDelegate::editProperties, this, &LayerList::showPropertiesOfIndex); + connect( + del, &LayerListDelegate::toggleVisibility, this, + &LayerList::setLayerVisibility); + connect( + del, &LayerListDelegate::editProperties, this, + &LayerList::showPropertiesOfIndex); m_view->setItemDelegate(del); } @@ -125,17 +140,39 @@ void LayerList::setCanvas(canvas::CanvasModel *canvas) m_aclmenu->setUserList(canvas->userlist()->onlineUsers()); - connect(canvas->layerlist(), &canvas::LayerListModel::modelAboutToBeReset, this, &LayerList::beforeLayerReset); - connect(canvas->layerlist(), &canvas::LayerListModel::modelReset, this, &LayerList::afterLayerReset); - connect(canvas->layerlist(), &canvas::LayerListModel::layersVisibleInFrameChanged, this, &LayerList::activeLayerVisibilityChanged); + connect( + canvas->layerlist(), &canvas::LayerListModel::modelAboutToBeReset, this, + &LayerList::beforeLayerReset); + connect( + canvas->layerlist(), &canvas::LayerListModel::modelReset, this, + &LayerList::afterLayerReset); + connect( + canvas->layerlist(), + &canvas::LayerListModel::layersVisibleInFrameChanged, this, + &LayerList::activeLayerVisibilityChanged); - connect(canvas->aclState(), &canvas::AclState::featureAccessChanged, this, &LayerList::onFeatureAccessChange); - connect(canvas->aclState(), &canvas::AclState::layerAclChanged, this, &LayerList::layerLockStatusChanged); - connect(canvas->aclState(), &canvas::AclState::localLockChanged, this, &LayerList::userLockStatusChanged); - connect(canvas->aclState(), &canvas::AclState::resetLockChanged, this, &LayerList::setDisabled); - connect(m_view->selectionModel(), SIGNAL(selectionChanged(QItemSelection,QItemSelection)), this, SLOT(selectionChanged(QItemSelection))); - connect(canvas, &canvas::CanvasModel::compatibilityModeChanged, this, &LayerList::updateLockedControls); - connect(canvas, &canvas::CanvasModel::compatibilityModeChanged, this, &LayerList::updateBlendModes); + connect( + canvas->aclState(), &canvas::AclState::featureAccessChanged, this, + &LayerList::onFeatureAccessChange); + connect( + canvas->aclState(), &canvas::AclState::layerAclChanged, this, + &LayerList::layerLockStatusChanged); + connect( + canvas->aclState(), &canvas::AclState::localLockChanged, this, + &LayerList::userLockStatusChanged); + connect( + canvas->aclState(), &canvas::AclState::resetLockChanged, this, + &LayerList::setDisabled); + connect( + m_view->selectionModel(), + SIGNAL(selectionChanged(QItemSelection, QItemSelection)), this, + SLOT(selectionChanged(QItemSelection))); + connect( + canvas, &canvas::CanvasModel::compatibilityModeChanged, this, + &LayerList::updateLockedControls); + connect( + canvas, &canvas::CanvasModel::compatibilityModeChanged, this, + &LayerList::updateBlendModes); // Init m_view->setEnabled(true); @@ -146,7 +183,8 @@ static void addLayerButton( QWidget *root, QHBoxLayout *layout, QAction *action, widgets::GroupedToolButton::GroupPosition position) { - widgets::GroupedToolButton *button = new widgets::GroupedToolButton{position, root}; + widgets::GroupedToolButton *button = + new widgets::GroupedToolButton{position, root}; button->setDefaultAction(action); layout->addWidget(button); } @@ -163,13 +201,23 @@ void LayerList::setLayerEditActions(const Actions &actions) QHBoxLayout *layout = new QHBoxLayout; layout->setSpacing(0); layout->setContentsMargins(titleBarWidget()->layout()->contentsMargins()); - addLayerButton(root, layout, m_actions.addLayer, widgets::GroupedToolButton::GroupLeft); - addLayerButton(root, layout, m_actions.addGroup, widgets::GroupedToolButton::GroupCenter); - addLayerButton(root, layout, m_actions.duplicate, widgets::GroupedToolButton::GroupCenter); - addLayerButton(root, layout, m_actions.merge, widgets::GroupedToolButton::GroupCenter); - addLayerButton(root, layout, m_actions.properties, widgets::GroupedToolButton::GroupRight); + addLayerButton( + root, layout, m_actions.addLayer, + widgets::GroupedToolButton::GroupLeft); + addLayerButton( + root, layout, m_actions.addGroup, + widgets::GroupedToolButton::GroupCenter); + addLayerButton( + root, layout, m_actions.duplicate, + widgets::GroupedToolButton::GroupCenter); + addLayerButton( + root, layout, m_actions.merge, widgets::GroupedToolButton::GroupCenter); + addLayerButton( + root, layout, m_actions.properties, + widgets::GroupedToolButton::GroupRight); layout->addStretch(); - addLayerButton(root, layout, m_actions.del, widgets::GroupedToolButton::NotGrouped); + addLayerButton( + root, layout, m_actions.del, widgets::GroupedToolButton::NotGrouped); root->layout()->addItem(layout); // Add the actions to the context menu @@ -235,11 +283,12 @@ void LayerList::onFeatureAccessChange(DP_Feature feature, bool canUse) { Q_UNUSED(canUse); switch(feature) { - case DP_FEATURE_EDIT_LAYERS: - case DP_FEATURE_OWN_LAYERS: - updateLockedControls(); - break; - default: break; + case DP_FEATURE_EDIT_LAYERS: + case DP_FEATURE_OWN_LAYERS: + updateLockedControls(); + break; + default: + break; } } @@ -262,7 +311,10 @@ void LayerList::updateLockedControls() } // Rest of the controls need a selection to work. - const bool enabled = !locked && m_selectedId && (canEdit || (ownLayers && (m_selectedId>>8) == m_canvas->localUserId())); + const bool enabled = + !locked && m_selectedId && + (canEdit || + (ownLayers && (m_selectedId >> 8) == m_canvas->localUserId())); m_lockButton->setEnabled(enabled); m_blendModeCombo->setEnabled(enabled); @@ -280,7 +332,8 @@ void LayerList::updateBlendModes(bool compatibilityMode) { QModelIndex index = currentSelection(); if(currentSelection().isValid()) { - const canvas::LayerListItem &layer = index.data().value(); + const canvas::LayerListItem &layer = + index.data().value(); dialogs::LayerProperties::updateBlendMode( m_blendModeCombo, layer.blend, layer.group, layer.isolated, compatibilityMode); @@ -316,7 +369,8 @@ void LayerList::selectLayerIndex(QModelIndex index, bool scrollTo) { if(index.isValid()) { m_view->selectionModel()->select( - index, QItemSelectionModel::SelectCurrent|QItemSelectionModel::Clear); + index, + QItemSelectionModel::SelectCurrent | QItemSelectionModel::Clear); if(scrollTo) { m_view->setExpanded(index, true); m_view->scrollTo(index); @@ -333,12 +387,14 @@ void LayerList::censorSelected(bool censor) { QModelIndex index = currentSelection(); if(index.isValid()) { - canvas::LayerListItem layer = index.data().value(); + canvas::LayerListItem layer = + index.data().value(); uint8_t flags = ChangeFlags() - .set(DP_MSG_LAYER_ATTRIBUTES_FLAGS_CENSOR, censor) - .update(layer.attributeFlags()); - drawdance::Message msg = drawdance::Message::makeLayerAttributes( - m_canvas->localUserId(), layer.id, 0, flags, layer.opacity * 255, layer.blend); + .set(DP_MSG_LAYER_ATTRIBUTES_FLAGS_CENSOR, censor) + .update(layer.attributeFlags()); + net::Message msg = net::makeLayerAttributesMessage( + m_canvas->localUserId(), layer.id, 0, flags, layer.opacity * 255, + layer.blend); emit layerCommands(1, &msg); } } @@ -348,40 +404,43 @@ void LayerList::setLayerVisibility(int layerId, bool visible) m_canvas->paintEngine()->setLayerVisibility(layerId, !visible); } -void LayerList::changeLayerAcl(bool lock, DP_AccessTier tier, QVector exclusive) +void LayerList::changeLayerAcl( + bool lock, DP_AccessTier tier, QVector exclusive) { const QModelIndex index = currentSelection(); if(index.isValid()) { uint16_t layerId = index.data(canvas::LayerListModel::IdRole).toInt(); uint8_t flags = (lock ? DP_ACL_ALL_LOCKED_BIT : 0) | uint8_t(tier); - drawdance::Message msg = drawdance::Message::makeLayerAcl( + net::Message msg = net::makeLayerAclMessage( m_canvas->localUserId(), layerId, flags, exclusive); emit layerCommands(1, &msg); } } -void LayerList::addLayerOrGroup(bool group, bool duplicateKeyFrame, bool keyFrame, int keyFrameOffset) +void LayerList::addLayerOrGroup( + bool group, bool duplicateKeyFrame, bool keyFrame, int keyFrameOffset) { canvas::LayerListModel *layers = m_canvas->layerlist(); Q_ASSERT(layers); const int id = layers->getAvailableLayerId(); - if(id==0) { - qWarning("Couldn't find a free ID for a new %s!", group ? "group" : "layer"); + if(id == 0) { + qWarning( + "Couldn't find a free ID for a new %s!", group ? "group" : "layer"); return; } uint8_t contextId = m_canvas->localUserId(); QModelIndex index = layers->layerIndex(m_selectedId); - drawdance::Message layerMsg; - drawdance::Message keyFrameMsg; - drawdance::Message moveMsg; + net::Message layerMsg; + net::Message keyFrameMsg; + net::Message moveMsg; bool compatibilityMode = m_canvas->isCompatibilityMode(); if(compatibilityMode) { uint16_t targetId = index.isValid() ? m_selectedId : 0; uint8_t flags = targetId == 0 ? 0 : DP_MSG_LAYER_CREATE_FLAGS_INSERT; - layerMsg = drawdance::Message::makeLayerCreate( + layerMsg = net::makeLayerCreateMessage( contextId, id, targetId, 0, flags, layers->getAvailableLayerName(tr("Layer"))); } else { @@ -396,19 +455,21 @@ void LayerList::addLayerOrGroup(bool group, bool duplicateKeyFrame, bool keyFram int moveId = intuitKeyFrameTarget( duplicateKeyFrame ? m_frame : -1, targetFrame, sourceId, targetId, flags); - keyFrameMsg = drawdance::Message::makeKeyFrameSet( + keyFrameMsg = net::makeKeyFrameSetMessage( contextId, m_trackId, targetFrame, id, 0, DP_MSG_KEY_FRAME_SET_SOURCE_LAYER); if(moveId != -1) { - moveMsg = drawdance::Message::makeLayerTreeMove( + moveMsg = net::makeLayerTreeMoveMessage( contextId, id, targetId, moveId); } } if(targetId == -1 && index.isValid()) { targetId = m_selectedId; - bool into = index.data(canvas::LayerListModel::IsGroupRole).toBool() - && (m_view->isExpanded(index) || index.data(canvas::LayerListModel::IsEmptyRole).toBool()); + bool into = + index.data(canvas::LayerListModel::IsGroupRole).toBool() && + (m_view->isExpanded(index) || + index.data(canvas::LayerListModel::IsEmptyRole).toBool()); if(into) { flags |= DP_MSG_LAYER_TREE_CREATE_FLAGS_INTO; } @@ -418,22 +479,22 @@ void LayerList::addLayerOrGroup(bool group, bool duplicateKeyFrame, bool keyFram if(sourceId != 0) { QModelIndex sourceIndex = layers->layerIndex(sourceId); if(sourceIndex.isValid()) { - baseName = sourceIndex.data(canvas::LayerListModel::TitleRole).toString(); + baseName = sourceIndex.data(canvas::LayerListModel::TitleRole) + .toString(); } } if(baseName.isEmpty()) { baseName = group ? tr("Group") : tr("Layer"); } - layerMsg = drawdance::Message::makeLayerTreeCreate( + layerMsg = net::makeLayerTreeCreateMessage( contextId, id, sourceId, qMax(0, targetId), 0, flags, layers->getAvailableLayerName(baseName)); } layers->setLayerIdToSelect(id); - drawdance::Message messages[] = { - drawdance::Message::makeUndoPoint(contextId), layerMsg, keyFrameMsg, - moveMsg}; + net::Message messages[] = { + net::makeUndoPointMessage(contextId), layerMsg, keyFrameMsg, moveMsg}; emit layerCommands( 2 + (keyFrameMsg.isNull() ? 0 : 1) + (moveMsg.isNull() ? 0 : 1), messages); @@ -506,34 +567,34 @@ int LayerList::intuitKeyFrameTarget( void LayerList::duplicateLayer() { const QModelIndex index = currentSelection(); - const canvas::LayerListItem layer = index.data().value(); + const canvas::LayerListItem layer = + index.data().value(); canvas::LayerListModel *layers = m_canvas->layerlist(); Q_ASSERT(layers); const int id = layers->getAvailableLayerId(); - if(id==0) { + if(id == 0) { qWarning("Couldn't find a free ID for duplicating layer!"); return; } uint8_t contextId = m_canvas->localUserId(); - drawdance::Message msg; + net::Message msg; if(m_canvas->isCompatibilityMode()) { - msg = drawdance::Message::makeLayerCreate( + msg = net::makeLayerCreateMessage( contextId, id, layer.id, 0, DP_MSG_LAYER_CREATE_FLAGS_COPY | DP_MSG_LAYER_CREATE_FLAGS_INSERT, layers->getAvailableLayerName(layer.title)); } else { - msg = drawdance::Message::makeLayerTreeCreate( + msg = net::makeLayerTreeCreateMessage( contextId, id, layer.id, layer.id, 0, 0, layers->getAvailableLayerName(layer.title)); } layers->setLayerIdToSelect(id); - drawdance::Message messages[] = { - drawdance::Message::makeUndoPoint(contextId), msg}; + net::Message messages[] = {net::makeUndoPointMessage(contextId), msg}; emit layerCommands(DP_ARRAY_LENGTH(messages), messages); } @@ -547,10 +608,11 @@ bool LayerList::canMergeCurrent() const if(index.data(canvas::LayerListModel::IsGroupRole).toBool()) { return true; } else { - const QModelIndex below = index.sibling(index.row()+1, 0); + const QModelIndex below = index.sibling(index.row() + 1, 0); return below.isValid() && - !below.data(canvas::LayerListModel::IsGroupRole).toBool() && - !m_canvas->aclState()->isLayerLocked(below.data(canvas::LayerListModel::IdRole).toInt()); + !below.data(canvas::LayerListModel::IsGroupRole).toBool() && + !m_canvas->aclState()->isLayerLocked( + below.data(canvas::LayerListModel::IdRole).toInt()); } } @@ -560,7 +622,8 @@ void LayerList::deleteSelected() if(!index.isValid()) return; - const canvas::LayerListItem &layer = index.data().value(); + const canvas::LayerListItem &layer = + index.data().value(); if(dpApp().settings().confirmLayerDelete()) { QMessageBox::StandardButton result = QMessageBox::question( this, tr("Delete Layer?"), @@ -573,17 +636,16 @@ void LayerList::deleteSelected() } uint8_t contextId = m_canvas->localUserId(); - drawdance::Message msg; + net::Message msg; if(m_canvas->isCompatibilityMode()) { - msg = drawdance::Message::makeLayerDelete( + msg = net::makeLayerDeleteMessage( contextId, index.data().value().id, false); } else { - msg = drawdance::Message::makeLayerTreeDelete( + msg = net::makeLayerTreeDeleteMessage( contextId, index.data().value().id, 0); } - drawdance::Message messages[] = { - drawdance::Message::makeUndoPoint(contextId), msg}; + net::Message messages[] = {net::makeUndoPointMessage(contextId), msg}; emit layerCommands(DP_ARRAY_LENGTH(messages), messages); } @@ -596,29 +658,28 @@ void LayerList::mergeSelected() uint8_t contextId = m_canvas->localUserId(); int layerId = index.data(canvas::LayerListModel::IdRole).toInt(); - drawdance::Message msg; + net::Message msg; if(m_canvas->isCompatibilityMode()) { - QModelIndex below = index.sibling(index.row()+1, 0); + QModelIndex below = index.sibling(index.row() + 1, 0); if(!below.isValid()) { return; } - msg = drawdance::Message::makeLayerDelete(contextId, layerId, true); + msg = net::makeLayerDeleteMessage(contextId, layerId, true); } else { int mergeId; if(index.data(canvas::LayerListModel::IsGroupRole).toBool()) { mergeId = layerId; } else { - QModelIndex below = index.sibling(index.row()+1, 0); + QModelIndex below = index.sibling(index.row() + 1, 0); if(!below.isValid()) { return; } mergeId = below.data(canvas::LayerListModel::IdRole).toInt(); } - msg = drawdance::Message::makeLayerTreeDelete(contextId, layerId, mergeId); + msg = net::makeLayerTreeDeleteMessage(contextId, layerId, mergeId); } - drawdance::Message messages[] = { - drawdance::Message::makeUndoPoint(contextId), msg}; + net::Message messages[] = {net::makeUndoPointMessage(contextId), msg}; emit layerCommands(DP_ARRAY_LENGTH(messages), messages); } @@ -642,30 +703,37 @@ void LayerList::showPropertiesOfIndex(QModelIndex index) dlg->setAttribute(Qt::WA_DeleteOnClose); dlg->setModal(false); - connect(m_canvas, &canvas::CanvasModel::compatibilityModeChanged, dlg, + connect( + m_canvas, &canvas::CanvasModel::compatibilityModeChanged, dlg, &dialogs::LayerProperties::setCompatibilityMode); - connect(dlg, &dialogs::LayerProperties::layerCommands, this, &LayerList::layerCommands); - connect(dlg, &dialogs::LayerProperties::visibilityChanged, this, &LayerList::setLayerVisibility); - connect(m_canvas->layerlist(), &canvas::LayerListModel::modelReset, dlg, [this, dlg]() { - const auto newIndex = m_canvas->layerlist()->layerIndex(dlg->layerId()); - if(!newIndex.isValid()) { - dlg->deleteLater(); - } - }); + connect( + dlg, &dialogs::LayerProperties::layerCommands, this, + &LayerList::layerCommands); + connect( + dlg, &dialogs::LayerProperties::visibilityChanged, this, + &LayerList::setLayerVisibility); + connect( + m_canvas->layerlist(), &canvas::LayerListModel::modelReset, dlg, + [this, dlg]() { + const auto newIndex = + m_canvas->layerlist()->layerIndex(dlg->layerId()); + if(!newIndex.isValid()) { + dlg->deleteLater(); + } + }); const int layerId = index.data(canvas::LayerListModel::IdRole).toInt(); dlg->setLayerItem( index.data().value(), layerCreatorName(layerId), - index.data(canvas::LayerListModel::IsDefaultRole).toBool() - ); - - const bool canEditAll = m_canvas->aclState()->canUseFeature(DP_FEATURE_EDIT_LAYERS); - const bool canEdit = canEditAll || - ( - m_canvas->aclState()->canUseFeature(DP_FEATURE_OWN_LAYERS) && - (layerId & 0xff00) >> 8 == m_canvas->localUserId() - ); + index.data(canvas::LayerListModel::IsDefaultRole).toBool()); + + const bool canEditAll = + m_canvas->aclState()->canUseFeature(DP_FEATURE_EDIT_LAYERS); + const bool canEdit = + canEditAll || + (m_canvas->aclState()->canUseFeature(DP_FEATURE_OWN_LAYERS) && + (layerId & 0xff00) >> 8 == m_canvas->localUserId()); dlg->setControlsEnabled(canEdit); dlg->setOpControlsEnabled(canEditAll); dlg->setCompatibilityMode(m_canvas->isCompatibilityMode()); @@ -684,10 +752,12 @@ void LayerList::showContextMenu(const QPoint &pos) void LayerList::beforeLayerReset() { - m_nearestToDeletedId = m_canvas->layerlist()->findNearestLayer(m_selectedId); + m_nearestToDeletedId = + m_canvas->layerlist()->findNearestLayer(m_selectedId); m_lastScrollPosition = m_view->verticalScrollBar()->value(); m_expandedGroups.clear(); - for(const canvas::LayerListItem &item : m_canvas->layerlist()->layerItems()) { + for(const canvas::LayerListItem &item : + m_canvas->layerlist()->layerItems()) { if(m_view->isExpanded(m_canvas->layerlist()->layerIndex(item.id))) { m_expandedGroups.insert(item.id); } @@ -711,7 +781,8 @@ void LayerList::afterLayerReset() // a selection change signal, so we have to call this manually. selectionChanged(QItemSelection{}); } else if(m_selectedId) { - const auto selectedIndex = m_canvas->layerlist()->layerIndex(m_selectedId); + const auto selectedIndex = + m_canvas->layerlist()->layerIndex(m_selectedId); if(selectedIndex.isValid()) { selectLayerIndex(selectedIndex); } else { @@ -782,7 +853,8 @@ void LayerList::selectionChanged(const QItemSelection &selected) void LayerList::updateUiFromSelection() { - const canvas::LayerListItem &layer = currentSelection().data().value(); + const canvas::LayerListItem &layer = + currentSelection().data().value(); m_noupdate = true; m_selectedId = layer.id; @@ -804,7 +876,9 @@ void LayerList::layerLockStatusChanged(int layerId) { if(m_selectedId == layerId) { const auto acl = m_canvas->aclState()->layerAcl(layerId); - m_lockButton->setChecked(acl.locked || acl.tier != DP_ACCESS_TIER_GUEST || !acl.exclusive.isEmpty()); + m_lockButton->setChecked( + acl.locked || acl.tier != DP_ACCESS_TIER_GUEST || + !acl.exclusive.isEmpty()); m_aclmenu->setAcl(acl.locked, int(acl.tier), acl.exclusive); emit activeLayerVisibilityChanged(); @@ -840,7 +914,8 @@ void LayerList::triggerUpdate() if(!index.isValid()) { return; } - const canvas::LayerListItem &layer = index.data().value(); + const canvas::LayerListItem &layer = + index.data().value(); DP_BlendMode mode; bool isolated; @@ -848,7 +923,8 @@ void LayerList::triggerUpdate() mode = layer.blend; isolated = layer.isolated; } else { - int blendModeData = m_blendModeCombo->itemData(m_updateBlendModeIndex).toInt(); + int blendModeData = + m_blendModeCombo->itemData(m_updateBlendModeIndex).toInt(); mode = blendModeData == -1 ? layer.blend : DP_BlendMode(blendModeData); isolated = layer.group && blendModeData != -1; m_updateBlendModeIndex = -1; @@ -866,8 +942,9 @@ void LayerList::triggerUpdate() (layer.censored ? DP_MSG_LAYER_ATTRIBUTES_FLAGS_CENSOR : 0) | (isolated ? DP_MSG_LAYER_ATTRIBUTES_FLAGS_ISOLATED : 0); - drawdance::Message msg = drawdance::Message::makeLayerAttributes( - m_canvas->localUserId(), layer.id, 0, flags, opacity * 255.0f + 0.5f, mode); + net::Message msg = net::makeLayerAttributesMessage( + m_canvas->localUserId(), layer.id, 0, flags, opacity * 255.0f + 0.5f, + mode); emit layerCommands(1, &msg); } diff --git a/src/desktop/docks/layerlistdock.h b/src/desktop/docks/layerlistdock.h index 14a4b1ea70..fe3c0375cb 100644 --- a/src/desktop/docks/layerlistdock.h +++ b/src/desktop/docks/layerlistdock.h @@ -2,43 +2,40 @@ #ifndef LAYERLISTDOCK_H #define LAYERLISTDOCK_H - extern "C" { #include } - #include "desktop/docks/dockbase.h" #include "desktop/scene/canvasview.h" #include -class QModelIndex; +class KisSliderSpinBox; +class QComboBox; class QItemSelection; class QMenu; +class QModelIndex; +class QSlider; class QTimer; class QTreeView; -class QComboBox; -class QSlider; -class KisSliderSpinBox; namespace canvas { - class CanvasModel; +class CanvasModel; } -namespace drawdance { - class Message; +namespace net { +class Message; } namespace widgets { - class GroupedToolButton; +class GroupedToolButton; } namespace docks { class LayerAclMenu; -class LayerList final : public DockBase -{ -Q_OBJECT +class LayerList final : public DockBase { + Q_OBJECT public: struct Actions { QAction *addLayer = nullptr; @@ -60,7 +57,7 @@ Q_OBJECT QActionGroup *layerKeyFrameGroup = nullptr; }; - LayerList(QWidget *parent=nullptr); + LayerList(QWidget *parent = nullptr); void setCanvas(canvas::CanvasModel *canvas); @@ -89,7 +86,7 @@ public slots: void activeLayerVisibilityChanged(); void fillSourceSet(int layerId); - void layerCommands(int count, const drawdance::Message *msgs); + void layerCommands(int count, const net::Message *msgs); private slots: void beforeLayerReset(); @@ -107,7 +104,8 @@ private slots: void showContextMenu(const QPoint &pos); void censorSelected(bool censor); void setLayerVisibility(int layerId, bool visible); - void changeLayerAcl(bool lock, DP_AccessTier tier, QVector exclusive); + void + changeLayerAcl(bool lock, DP_AccessTier tier, QVector exclusive); void layerLockStatusChanged(int layerId); void userLockStatusChanged(bool); @@ -124,11 +122,14 @@ private slots: void updateUiFromSelection(); - void addLayerOrGroup(bool group, bool duplicateKeyFrame, bool keyFrame, int keyFrameOffset); - int intuitKeyFrameTarget(int sourceFrame, int targetFrame, int &sourceId, int &targetId, uint8_t &flags); + void addLayerOrGroup( + bool group, bool duplicateKeyFrame, bool keyFrame, int keyFrameOffset); + int intuitKeyFrameTarget( + int sourceFrame, int targetFrame, int &sourceId, int &targetId, + uint8_t &flags); QModelIndex currentSelection() const; - void selectLayerIndex(QModelIndex index, bool scrollTo=false); + void selectLayerIndex(QModelIndex index, bool scrollTo = false); QString layerCreatorName(uint16_t layerId) const; @@ -165,4 +166,3 @@ private slots: } #endif - diff --git a/src/desktop/docks/timeline.cpp b/src/desktop/docks/timeline.cpp index 68dac4e270..7fd9f29d91 100644 --- a/src/desktop/docks/timeline.cpp +++ b/src/desktop/docks/timeline.cpp @@ -1,5 +1,4 @@ // SPDX-License-Identifier: GPL-3.0-or-later - #include "desktop/docks/timeline.h" #include "desktop/docks/titlewidget.h" #include "desktop/widgets/timelinewidget.h" @@ -7,8 +6,7 @@ #include "libclient/canvas/canvasmodel.h" #include "libclient/canvas/documentmetadata.h" #include "libclient/canvas/timelinemodel.h" -#include "libclient/drawdance/message.h" - +#include "libclient/net/message.h" #include #include #include diff --git a/src/desktop/docks/timeline.h b/src/desktop/docks/timeline.h index c3392ae142..38c836a51e 100644 --- a/src/desktop/docks/timeline.h +++ b/src/desktop/docks/timeline.h @@ -1,8 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later - #ifndef TIMELINE_DOCK_H #define TIMELINE_DOCK_H - #include "desktop/docks/dockbase.h" #include "desktop/widgets/groupedtoolbutton.h" #include "desktop/widgets/timelinewidget.h" @@ -15,7 +13,7 @@ namespace canvas { class CanvasModel; } -namespace drawdance { +namespace net { class Message; } @@ -50,7 +48,7 @@ public slots: void setFeatureAccess(bool access); signals: - void timelineEditCommands(int count, const drawdance::Message *msgs); + void timelineEditCommands(int count, const net::Message *msgs); void trackSelected(int frame); void frameSelected(int frame); void layerSelected(int layerId); diff --git a/src/desktop/mainwindow.cpp b/src/desktop/mainwindow.cpp index 3d0c86b2f3..4ffde0a2b4 100644 --- a/src/desktop/mainwindow.cpp +++ b/src/desktop/mainwindow.cpp @@ -98,7 +98,6 @@ static constexpr auto CTRL_KEY = Qt::CTRL; #include "desktop/filewrangler.h" #include "libclient/export/animationsaverrunnable.h" -#include "libshared/record/reader.h" #include "libclient/drawdance/eventlog.h" #include "libclient/drawdance/perf.h" @@ -1040,7 +1039,7 @@ void MainWindow::requestUserInfo(int userId) { net::Client *client = m_doc->client(); QJsonObject info{{"type", "request_user_info"}}; - client->sendMessage(drawdance::Message::makeUserInfo( + client->sendMessage(net::makeUserInfoMessage( client->myId(), userId, QJsonDocument{info})); } @@ -1066,7 +1065,7 @@ void MainWindow::sendUserInfo(int userId) {"pressure_curve", m_view->pressureCurve().toString()}, }; net::Client *client = m_doc->client(); - client->sendMessage(drawdance::Message::makeUserInfo( + client->sendMessage(net::makeUserInfoMessage( client->myId(), userId, QJsonDocument{info})); } @@ -1973,7 +1972,7 @@ void MainWindow::resetSession() connect(dlg, &dialogs::ResetDialog::resetSelected, this, [this, dlg]() { canvas::CanvasModel *canvas = m_doc->canvas(); if(canvas->aclState()->amOperator()) { - drawdance::MessageList snapshot = dlg->getResetImage(); + net::MessageList snapshot = dlg->getResetImage(); canvas->amendSnapshotMetadata( snapshot, true, DP_ACL_STATE_RESET_IMAGE_SESSION_RESET_FLAGS); m_doc->sendResetSession(snapshot); @@ -2666,9 +2665,9 @@ void MainWindow::clearOrDelete() if(a>0) { net::Client *client = m_doc->client(); uint8_t contextId = client->myId(); - drawdance::Message messages[] = { - drawdance::Message::makeUndoPoint(contextId), - drawdance::Message::makeAnnotationDelete(contextId, a), + net::Message messages[] = { + net::makeUndoPointMessage(contextId), + net::makeAnnotationDeleteMessage(contextId, a), }; client->sendMessages(DP_ARRAY_LENGTH(messages), messages); return; @@ -2855,7 +2854,7 @@ void MainWindow::changeUndoDepthLimit() if(dlg.exec() == QDialog::Accepted) { int undoDepthLimit = dlg.undoDepthLimit(); if(undoDepthLimit != previousUndoDepthLimit) { - m_doc->client()->sendMessage(drawdance::Message::makeUndoDepth( + m_doc->client()->sendMessage(net::makeUndoDepthMessage( m_doc->canvas()->localUserId(), undoDepthLimit)); } } diff --git a/src/desktop/toolwidgets/annotationsettings.cpp b/src/desktop/toolwidgets/annotationsettings.cpp index 8e2a85336f..dd31cc6b42 100644 --- a/src/desktop/toolwidgets/annotationsettings.cpp +++ b/src/desktop/toolwidgets/annotationsettings.cpp @@ -1,23 +1,19 @@ // SPDX-License-Identifier: GPL-3.0-or-later - #include "desktop/toolwidgets/annotationsettings.h" -#include "libclient/tools/toolcontroller.h" +#include "desktop/scene/annotationitem.h" +#include "desktop/scene/canvasscene.h" +#include "desktop/utils/qtguicompat.h" +#include "desktop/widgets/groupedtoolbutton.h" #include "libclient/canvas/canvasmodel.h" #include "libclient/canvas/userlist.h" -#include "desktop/scene/canvasscene.h" -#include "desktop/scene/annotationitem.h" #include "libclient/net/client.h" -#include "desktop/widgets/groupedtoolbutton.h" -#include "desktop/utils/qtguicompat.h" - - +#include "libclient/tools/toolcontroller.h" #include "ui_textsettings.h" - #include #include -#include -#include #include +#include +#include namespace tools { @@ -25,8 +21,11 @@ static const char *HALIGN_PROP = "HALIGN"; static const char *VALIGN_PROP = "VALIGN"; AnnotationSettings::AnnotationSettings(ToolController *ctrl, QObject *parent) - : ToolSettings(ctrl, parent), m_ui(nullptr), m_headerWidget(nullptr), - m_selectionId(0), m_noupdate(false) + : ToolSettings(ctrl, parent) + , m_ui(nullptr) + , m_headerWidget(nullptr) + , m_selectionId(0) + , m_noupdate(false) { } @@ -47,14 +46,17 @@ QWidget *AnnotationSettings::createUiWidget(QWidget *parent) m_ui->headerLayout->setContentsMargins(0, 0, 1, 0); m_headerWidget->setLayout(m_ui->headerLayout); - m_protectedAction = new QAction(QIcon::fromTheme("object-locked"), tr("Protect"), this); + m_protectedAction = + new QAction(QIcon::fromTheme("object-locked"), tr("Protect"), this); m_protectedAction->setCheckable(true); m_ui->protectButton->setDefaultAction(m_protectedAction); - QAction *mergeAction = new QAction(QIcon::fromTheme("arrow-down-double"), tr("Merge"), this); + QAction *mergeAction = + new QAction(QIcon::fromTheme("arrow-down-double"), tr("Merge"), this); m_ui->mergeButton->setDefaultAction(mergeAction); - QAction *deleteAction = new QAction(QIcon::fromTheme("list-remove"), tr("Delete"), this); + QAction *deleteAction = + new QAction(QIcon::fromTheme("list-remove"), tr("Delete"), this); m_ui->deleteButton->setDefaultAction(deleteAction); m_editActions = new QActionGroup(this); @@ -70,53 +72,97 @@ QWidget *AnnotationSettings::createUiWidget(QWidget *parent) // Horizontal alignment options QMenu *halignMenu = new QMenu(parent); - halignMenu->addAction(QIcon::fromTheme("format-justify-left"), tr("Left"))->setProperty(HALIGN_PROP, Qt::AlignLeft); - halignMenu->addAction(QIcon::fromTheme("format-justify-center"), tr("Center"))->setProperty(HALIGN_PROP, Qt::AlignCenter); - halignMenu->addAction(QIcon::fromTheme("format-justify-fill"), tr("Justify"))->setProperty(HALIGN_PROP, Qt::AlignJustify); - halignMenu->addAction(QIcon::fromTheme("format-justify-right"), tr("Right"))->setProperty(HALIGN_PROP, Qt::AlignRight); + halignMenu->addAction(QIcon::fromTheme("format-justify-left"), tr("Left")) + ->setProperty(HALIGN_PROP, Qt::AlignLeft); + halignMenu + ->addAction(QIcon::fromTheme("format-justify-center"), tr("Center")) + ->setProperty(HALIGN_PROP, Qt::AlignCenter); + halignMenu + ->addAction(QIcon::fromTheme("format-justify-fill"), tr("Justify")) + ->setProperty(HALIGN_PROP, Qt::AlignJustify); + halignMenu->addAction(QIcon::fromTheme("format-justify-right"), tr("Right")) + ->setProperty(HALIGN_PROP, Qt::AlignRight); m_ui->halign->setIcon(halignMenu->actions().constFirst()->icon()); - connect(halignMenu, &QMenu::triggered, this, &AnnotationSettings::changeAlignment); + connect( + halignMenu, &QMenu::triggered, this, + &AnnotationSettings::changeAlignment); m_ui->halign->setMenu(halignMenu); // Vertical alignment options QMenu *valignMenu = new QMenu(parent); - valignMenu->addAction(QIcon::fromTheme("format-align-vertical-top"), tr("Top"))->setProperty(VALIGN_PROP, 0); - valignMenu->addAction(QIcon::fromTheme("format-align-vertical-center"), tr("Center"))->setProperty(VALIGN_PROP, DP_MSG_ANNOTATION_EDIT_FLAGS_VALIGN_CENTER); - valignMenu->addAction(QIcon::fromTheme("format-align-vertical-bottom"), tr("Bottom"))->setProperty(VALIGN_PROP, DP_MSG_ANNOTATION_EDIT_FLAGS_VALIGN_BOTTOM); + valignMenu + ->addAction(QIcon::fromTheme("format-align-vertical-top"), tr("Top")) + ->setProperty(VALIGN_PROP, 0); + valignMenu + ->addAction( + QIcon::fromTheme("format-align-vertical-center"), tr("Center")) + ->setProperty(VALIGN_PROP, DP_MSG_ANNOTATION_EDIT_FLAGS_VALIGN_CENTER); + valignMenu + ->addAction( + QIcon::fromTheme("format-align-vertical-bottom"), tr("Bottom")) + ->setProperty(VALIGN_PROP, DP_MSG_ANNOTATION_EDIT_FLAGS_VALIGN_BOTTOM); m_ui->valign->setIcon(valignMenu->actions().constFirst()->icon()); - connect(valignMenu, &QMenu::triggered, this, &AnnotationSettings::changeAlignment); + connect( + valignMenu, &QMenu::triggered, this, + &AnnotationSettings::changeAlignment); m_ui->valign->setMenu(valignMenu); m_ui->btnTextColor->setColor(Qt::black); m_ui->btnBackground->setColor(Qt::transparent); // Editor events - connect(m_ui->content, &QTextEdit::textChanged, this, &AnnotationSettings::applyChanges); - connect(m_ui->content, &QTextEdit::cursorPositionChanged, this, &AnnotationSettings::updateStyleButtons); - - connect(m_ui->btnBackground, &widgets::ColorButton::colorChanged, this, &AnnotationSettings::setEditorBackgroundColor); - connect(m_ui->btnBackground, &widgets::ColorButton::colorChanged, this, &AnnotationSettings::applyChanges); - - connect(deleteAction, &QAction::triggered, this, &AnnotationSettings::removeAnnotation); + connect( + m_ui->content, &QTextEdit::textChanged, this, + &AnnotationSettings::applyChanges); + connect( + m_ui->content, &QTextEdit::cursorPositionChanged, this, + &AnnotationSettings::updateStyleButtons); + + connect( + m_ui->btnBackground, &widgets::ColorButton::colorChanged, this, + &AnnotationSettings::setEditorBackgroundColor); + connect( + m_ui->btnBackground, &widgets::ColorButton::colorChanged, this, + &AnnotationSettings::applyChanges); + + connect( + deleteAction, &QAction::triggered, this, + &AnnotationSettings::removeAnnotation); connect(mergeAction, &QAction::triggered, this, &AnnotationSettings::bake); - connect(m_protectedAction, &QAction::triggered, this, &AnnotationSettings::saveChanges); - - connect(m_ui->font, SIGNAL(currentIndexChanged(int)), this, SLOT(updateFontIfUniform())); - connect(m_ui->size, SIGNAL(valueChanged(double)), this, SLOT(updateFontIfUniform())); - connect(m_ui->btnTextColor, &widgets::ColorButton::colorChanged, this, &AnnotationSettings::updateFontIfUniform); + connect( + m_protectedAction, &QAction::triggered, this, + &AnnotationSettings::saveChanges); + + connect( + m_ui->font, SIGNAL(currentIndexChanged(int)), this, + SLOT(updateFontIfUniform())); + connect( + m_ui->size, SIGNAL(valueChanged(double)), this, + SLOT(updateFontIfUniform())); + connect( + m_ui->btnTextColor, &widgets::ColorButton::colorChanged, this, + &AnnotationSettings::updateFontIfUniform); // Intra-editor connections that couldn't be made in the UI designer - connect(m_ui->bold, &QToolButton::toggled, this, &AnnotationSettings::toggleBold); - connect(m_ui->strikethrough, &QToolButton::toggled, this, &AnnotationSettings::toggleStrikethrough); + connect( + m_ui->bold, &QToolButton::toggled, this, + &AnnotationSettings::toggleBold); + connect( + m_ui->strikethrough, &QToolButton::toggled, this, + &AnnotationSettings::toggleStrikethrough); - connect(m_updatetimer, &QTimer::timeout, this, &AnnotationSettings::saveChanges); + connect( + m_updatetimer, &QTimer::timeout, this, + &AnnotationSettings::saveChanges); // Select a nice default font QStringList defaultFonts; - defaultFonts << "Arial" << "Helvetica" << "Sans Serif"; + defaultFonts << "Arial" + << "Helvetica" + << "Sans Serif"; for(const QString &df : defaultFonts) { int i = m_ui->font->findText(df, Qt::MatchFixedString); - if(i>=0) { + if(i >= 0) { m_ui->font->setCurrentIndex(i); break; } @@ -142,18 +188,10 @@ QWidget *AnnotationSettings::getHeaderWidget() void AnnotationSettings::setUiEnabled(bool enabled) { QWidget *widgets[] = { - m_ui->content, - m_ui->btnBackground, - m_ui->btnTextColor, - m_ui->halign, - m_ui->valign, - m_ui->bold, - m_ui->italic, - m_ui->underline, - m_ui->strikethrough, - m_ui->font, - m_ui->size - }; + m_ui->content, m_ui->btnBackground, m_ui->btnTextColor, + m_ui->halign, m_ui->valign, m_ui->bold, + m_ui->italic, m_ui->underline, m_ui->strikethrough, + m_ui->font, m_ui->size}; for(QWidget *w : widgets) { w->setEnabled(enabled); } @@ -168,10 +206,9 @@ void AnnotationSettings::setEditorBackgroundColor(const QColor &color) { // Blend transparent colors with white const QColor c = QColor::fromRgbF( - color.redF() * color.alphaF() + (1-color.alphaF()), - color.greenF() * color.alphaF() + (1-color.alphaF()), - color.blueF() * color.alphaF() + (1-color.alphaF()) - ); + color.redF() * color.alphaF() + (1 - color.alphaF()), + color.greenF() * color.alphaF() + (1 - color.alphaF()), + color.blueF() * color.alphaF() + (1 - color.alphaF())); // We need to use the stylesheet because native styles ignore the palette. m_ui->content->setStyleSheet( @@ -182,11 +219,20 @@ void AnnotationSettings::updateStyleButtons() { QTextBlockFormat bf = m_ui->content->textCursor().blockFormat(); switch(bf.alignment()) { - case Qt::AlignLeft: m_ui->halign->setIcon(QIcon::fromTheme("format-justify-left")); break; - case Qt::AlignCenter: m_ui->halign->setIcon(QIcon::fromTheme("format-justify-center")); break; - case Qt::AlignJustify: m_ui->halign->setIcon(QIcon::fromTheme("format-justify-fill")); break; - case Qt::AlignRight: m_ui->halign->setIcon(QIcon::fromTheme("format-justify-right")); break; - default: break; + case Qt::AlignLeft: + m_ui->halign->setIcon(QIcon::fromTheme("format-justify-left")); + break; + case Qt::AlignCenter: + m_ui->halign->setIcon(QIcon::fromTheme("format-justify-center")); + break; + case Qt::AlignJustify: + m_ui->halign->setIcon(QIcon::fromTheme("format-justify-fill")); + break; + case Qt::AlignRight: + m_ui->halign->setIcon(QIcon::fromTheme("format-justify-right")); + break; + default: + break; } QTextCharFormat cf = m_ui->content->textCursor().charFormat(); @@ -247,7 +293,7 @@ void AnnotationSettings::updateFontIfUniform() QTextBlock b = doc->firstBlock(); QTextCharFormat fmt1; - bool first=true; + bool first = true; // Check all character formats in all blocks. If they are the same, // we can reset the font for the wole document. @@ -260,8 +306,10 @@ void AnnotationSettings::updateFontIfUniform() first = false; } else { - uniformFontFamily &= compat::fontFamily(fr.format) == compat::fontFamily(fmt1); - uniformSize &= qFuzzyCompare(fr.format.fontPointSize(), fmt1.fontPointSize()); + uniformFontFamily &= + compat::fontFamily(fr.format) == compat::fontFamily(fmt1); + uniformSize &= qFuzzyCompare( + fr.format.fontPointSize(), fmt1.fontPointSize()); uniformColor &= fr.format.foreground() == fmt1.foreground(); } } @@ -271,9 +319,10 @@ void AnnotationSettings::updateFontIfUniform() resetContentFont(uniformFontFamily, uniformSize, uniformColor); } -void AnnotationSettings::resetContentFont(bool resetFamily, bool resetSize, bool resetColor) +void AnnotationSettings::resetContentFont( + bool resetFamily, bool resetSize, bool resetColor) { - if(!(resetFamily|resetSize|resetColor)) + if(!(resetFamily | resetSize | resetColor)) return; QTextCursor cursor(m_ui->content->document()); @@ -300,7 +349,7 @@ void AnnotationSettings::setFontFamily(QTextCharFormat &fmt) void AnnotationSettings::setSelectionId(uint16_t id) { m_noupdate = true; - setUiEnabled(id>0); + setUiEnabled(id > 0); m_selectionId = id; if(id) { @@ -318,23 +367,29 @@ void AnnotationSettings::setSelectionId(uint16_t id) int align = 0; switch(a->valign()) { case 0: - m_ui->valign->setIcon(QIcon::fromTheme("format-align-vertical-top")); + m_ui->valign->setIcon( + QIcon::fromTheme("format-align-vertical-top")); break; case 1: - m_ui->valign->setIcon(QIcon::fromTheme("format-align-vertical-center")); + m_ui->valign->setIcon( + QIcon::fromTheme("format-align-vertical-center")); align = DP_MSG_ANNOTATION_EDIT_FLAGS_VALIGN_CENTER; break; case 2: - m_ui->valign->setIcon(QIcon::fromTheme("format-align-vertical-bottom")); + m_ui->valign->setIcon( + QIcon::fromTheme("format-align-vertical-bottom")); align = DP_MSG_ANNOTATION_EDIT_FLAGS_VALIGN_BOTTOM; break; } m_ui->valign->setProperty(VALIGN_PROP, align); - m_ui->creatorLabel->setText(controller()->model()->userlist()->getUsername(a->userId())); + m_ui->creatorLabel->setText( + controller()->model()->userlist()->getUsername(a->userId())); m_protectedAction->setChecked(a->protect()); - const bool opOrOwner = controller()->model()->aclState()->amOperator() || (a->id() >> 8) == controller()->client()->myId(); + const bool opOrOwner = + controller()->model()->aclState()->amOperator() || + (a->id() >> 8) == controller()->client()->myId(); m_protectedAction->setEnabled(opOrOwner); if(a->protect() && !opOrOwner) @@ -346,7 +401,7 @@ void AnnotationSettings::setSelectionId(uint16_t id) void AnnotationSettings::setFocusAt(int cursorPos) { m_ui->content->setFocus(); - if(cursorPos>=0) { + if(cursorPos >= 0) { QTextCursor c = m_ui->content->textCursor(); c.setPosition(cursorPos); m_ui->content->setTextCursor(c); @@ -382,11 +437,13 @@ void AnnotationSettings::saveChanges() net::Client *client = controller()->client(); uint8_t contextId = client->myId(); - uint8_t flags = - (m_protectedAction->isChecked() ? DP_MSG_ANNOTATION_EDIT_FLAGS_PROTECT : 0) | - uint8_t(m_ui->valign->property(VALIGN_PROP).toInt()); - client->sendMessage(drawdance::Message::makeAnnotationEdit( - contextId, selected(), m_ui->btnBackground->color().rgba(), flags, 0, content)); + uint8_t flags = (m_protectedAction->isChecked() + ? DP_MSG_ANNOTATION_EDIT_FLAGS_PROTECT + : 0) | + uint8_t(m_ui->valign->property(VALIGN_PROP).toInt()); + client->sendMessage(net::makeAnnotationEditMessage( + contextId, selected(), m_ui->btnBackground->color().rgba(), flags, + 0, content)); } } @@ -395,9 +452,9 @@ void AnnotationSettings::removeAnnotation() Q_ASSERT(selected()); net::Client *client = controller()->client(); uint8_t contextId = client->myId(); - drawdance::Message messages[] = { - drawdance::Message::makeUndoPoint(contextId), - drawdance::Message::makeAnnotationDelete(contextId, selected()), + net::Message messages[] = { + net::makeUndoPointMessage(contextId), + net::makeAnnotationDeleteMessage(contextId, selected()), }; client->sendMessages(DP_ARRAY_LENGTH(messages), messages); } @@ -416,14 +473,13 @@ void AnnotationSettings::bake() int layer = controller()->activeLayer(); QRect rect = a->rect().toRect(); QImage img = a->toImage(); - drawdance::MessageList msgs = { - drawdance::Message::makeUndoPoint(contextId), - drawdance::Message::makeAnnotationDelete(contextId, selected()), + net::MessageList msgs = { + net::makeUndoPointMessage(contextId), + net::makeAnnotationDeleteMessage(contextId, selected()), }; - drawdance::Message::makePutImages( + net::makePutImageMessages( msgs, contextId, layer, DP_BLEND_MODE_NORMAL, rect.x(), rect.y(), img); client->sendMessages(msgs.count(), msgs.data()); } } - diff --git a/src/desktop/toolwidgets/annotationsettings.h b/src/desktop/toolwidgets/annotationsettings.h index 4037b4cd18..14ef926105 100644 --- a/src/desktop/toolwidgets/annotationsettings.h +++ b/src/desktop/toolwidgets/annotationsettings.h @@ -1,18 +1,16 @@ // SPDX-License-Identifier: GPL-3.0-or-later - #ifndef TOOLSETTINGS_ANNOTATION_H #define TOOLSETTINGS_ANNOTATION_H - #include "desktop/toolwidgets/toolsettings.h" -class Ui_TextSettings; class QAction; class QActionGroup; class QTextCharFormat; class QTimer; +class Ui_TextSettings; namespace drawingboard { - class CanvasScene; +class CanvasScene; } namespace tools { @@ -26,7 +24,7 @@ namespace tools { class AnnotationSettings final : public ToolSettings { Q_OBJECT public: - AnnotationSettings(ToolController *ctrl, QObject *parent=nullptr); + AnnotationSettings(ToolController *ctrl, QObject *parent = nullptr); ~AnnotationSettings() override; QString toolType() const override { return QStringLiteral("annotation"); } @@ -94,4 +92,3 @@ private slots: } #endif - diff --git a/src/desktop/widgets/timelinewidget.cpp b/src/desktop/widgets/timelinewidget.cpp index c98193afa5..e554b492dd 100644 --- a/src/desktop/widgets/timelinewidget.cpp +++ b/src/desktop/widgets/timelinewidget.cpp @@ -1,9 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later - extern "C" { #include } - #include "desktop/dialogs/keyframepropertiesdialog.h" #include "desktop/utils/qtguicompat.h" #include "desktop/utils/widgetutils.h" @@ -13,8 +11,7 @@ extern "C" { #include "libclient/canvas/layerlist.h" #include "libclient/canvas/paintengine.h" #include "libclient/canvas/timelinemodel.h" -#include "libclient/drawdance/message.h" - +#include "libclient/net/message.h" #include #include #include @@ -571,7 +568,7 @@ int TimelineWidget::currentFrame() const void TimelineWidget::changeFramerate(int framerate) { emitCommand([&](uint8_t contextId) { - return drawdance::Message::makeSetMetadataInt( + return net::makeSetMetadataIntMessage( contextId, DP_MSG_SET_METADATA_INT_FIELD_FRAMERATE, framerate); }); } @@ -579,7 +576,7 @@ void TimelineWidget::changeFramerate(int framerate) void TimelineWidget::changeFrameCount(int frameCount) { emitCommand([&](uint8_t contextId) { - return drawdance::Message::makeSetMetadataInt( + return net::makeSetMetadataIntMessage( contextId, DP_MSG_SET_METADATA_INT_FIELD_FRAME_COUNT, frameCount); }); } @@ -1091,7 +1088,7 @@ void TimelineWidget::dropEvent(QDropEvent *event) trackIds.move(source, target); emitCommand([&](uint8_t contextId) { - return drawdance::Message::makeTrackOrder(contextId, trackIds); + return net::makeTrackOrderMessage(contextId, trackIds); }); } else if(dragType == int(Drag::KeyFrame)) { int sourceTrackId = mimeData->property("trackId").toInt(); @@ -1104,12 +1101,12 @@ void TimelineWidget::dropEvent(QDropEvent *event) applyMouseTarget(nullptr, target, false); emitCommand([&](uint8_t contextId) { if(event->dropAction() == Qt::CopyAction) { - return drawdance::Message::makeKeyFrameSet( + return net::makeKeyFrameSetMessage( contextId, target.trackId, target.frameIndex, sourceTrackId, sourceFrameIndex, DP_MSG_KEY_FRAME_SET_SOURCE_KEY_FRAME); } else { - return drawdance::Message::makeKeyFrameDelete( + return net::makeKeyFrameDeleteMessage( contextId, sourceTrackId, sourceFrameIndex, target.trackId, target.frameIndex); } @@ -1279,7 +1276,7 @@ void TimelineWidget::deleteKeyFrame() const canvas::TimelineKeyFrame *keyFrame = d->currentKeyFrame(); if(keyFrame) { emitCommand([&](uint8_t contextId) { - return drawdance::Message::makeKeyFrameDelete( + return net::makeKeyFrameDeleteMessage( contextId, d->currentTrackId, keyFrame->frameIndex, 0, 0); }); } @@ -1311,7 +1308,7 @@ void TimelineWidget::addTrack() d->nextTrackId = trackId; const canvas::TimelineTrack *track = d->currentTrack(); emitCommand([&](uint8_t contextId) { - return drawdance::Message::makeTrackCreate( + return net::makeTrackCreateMessage( contextId, trackId, track ? track->id : 0, 0, timeline->getAvailableTrackName(tr("Track"))); }); @@ -1351,7 +1348,7 @@ void TimelineWidget::duplicateTrack() d->nextTrackId = trackId; emitCommand([&](uint8_t contextId) { - return drawdance::Message::makeTrackCreate( + return net::makeTrackCreateMessage( contextId, trackId, source->id, source->id, timeline->getAvailableTrackName(source->title)); }); @@ -1374,7 +1371,7 @@ void TimelineWidget::retitleTrack() source->title, &ok); if(ok && !(title = title.trimmed()).isEmpty()) { emitCommand([&](uint8_t contextId) { - return drawdance::Message::makeTrackRetitle( + return net::makeTrackRetitleMessage( contextId, d->currentTrackId, title); }); } @@ -1394,7 +1391,7 @@ void TimelineWidget::deleteTrack() } emitCommand([&](uint8_t contextId) { - return drawdance::Message::makeTrackDelete(contextId, trackId); + return net::makeTrackDeleteMessage(contextId, trackId); }); } @@ -1568,7 +1565,7 @@ void TimelineWidget::setCurrent( void TimelineWidget::setKeyFrame(int layerId) { emitCommand([&](uint8_t contextId) { - return drawdance::Message::makeKeyFrameSet( + return net::makeKeyFrameSetMessage( contextId, d->currentTrackId, d->currentFrame, layerId, 0, DP_MSG_KEY_FRAME_SET_SOURCE_LAYER); }); @@ -1584,11 +1581,11 @@ void TimelineWidget::setKeyFrameProperties( if(titleChanged || layersChanged) { uint8_t contextId = d->canvas->localUserId(); - drawdance::Message messages[3]; + net::Message messages[3]; int fill = 0; - messages[fill++] = drawdance::Message::makeUndoPoint(contextId); + messages[fill++] = net::makeUndoPointMessage(contextId); if(titleChanged) { - messages[fill++] = drawdance::Message::makeKeyFrameRetitle( + messages[fill++] = net::makeKeyFrameRetitleMessage( contextId, trackId, frame, title); } if(layersChanged) { @@ -1602,7 +1599,7 @@ void TimelineWidget::setKeyFrameProperties( it.value() ? DP_KEY_FRAME_LAYER_REVEALED : DP_KEY_FRAME_LAYER_HIDDEN); } - messages[fill++] = drawdance::Message::makeKeyFrameLayerAttributes( + messages[fill++] = net::makeKeyFrameLayerAttributesMessage( contextId, trackId, frame, layers); } emit timelineEditCommands(fill, messages); @@ -1639,11 +1636,11 @@ void TimelineWidget::changeFrameExposure(int direction) } uint8_t contextId = d->canvas->localUserId(); - QVector messages; + QVector messages; messages.reserve(frameIndexes.size() + 1); - messages.append(drawdance::Message::makeUndoPoint(contextId)); + messages.append(net::makeUndoPointMessage(contextId)); for(int frameIndex : frameIndexes) { - messages.append(drawdance::Message::makeKeyFrameDelete( + messages.append(net::makeKeyFrameDeleteMessage( contextId, d->currentTrackId, frameIndex, d->currentTrackId, frameIndex + direction)); } @@ -1836,12 +1833,12 @@ void TimelineWidget::executeTargetAction(const Target &target) } void TimelineWidget::emitCommand( - std::function getMessage) + std::function getMessage) { if(d->editable) { uint8_t contextId = d->canvas->localUserId(); - drawdance::Message messages[] = { - drawdance::Message::makeUndoPoint(contextId), + net::Message messages[] = { + net::makeUndoPointMessage(contextId), getMessage(contextId), }; emit timelineEditCommands(DP_ARRAY_LENGTH(messages), messages); diff --git a/src/desktop/widgets/timelinewidget.h b/src/desktop/widgets/timelinewidget.h index 041af04e3d..ac8df755f1 100644 --- a/src/desktop/widgets/timelinewidget.h +++ b/src/desktop/widgets/timelinewidget.h @@ -1,8 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later - #ifndef TIMELINEWIDGET_H #define TIMELINEWIDGET_H - #include #include @@ -13,7 +11,7 @@ namespace canvas { class CanvasModel; } -namespace drawdance { +namespace net { class Message; } @@ -82,7 +80,7 @@ public slots: void changeFrameCount(int frameCount); signals: - void timelineEditCommands(int count, const drawdance::Message *msgs); + void timelineEditCommands(int count, const net::Message *msgs); void trackSelected(int trackId); void frameSelected(int frame); void layerSelected(int layerId); @@ -158,7 +156,7 @@ private slots: void applyMouseTarget(QMouseEvent *event, const Target &target, bool press); void executeTargetAction(const Target &target); - void emitCommand(std::function getMessage); + void emitCommand(std::function getMessage); static void setCheckedSignalBlocked(QAction *action, bool checked); diff --git a/src/drawdance/generators/protogen/messages.c.jinja b/src/drawdance/generators/protogen/messages.c.jinja index 319677f303..968f9585b8 100644 --- a/src/drawdance/generators/protogen/messages.c.jinja +++ b/src/drawdance/generators/protogen/messages.c.jinja @@ -143,21 +143,25 @@ bool DP_message_type_parse_multiline_tuples(DP_MessageType type) } -DP_Message *DP_message_deserialize_body(int type, unsigned int context_id, const unsigned char *buf, size_t length) +DP_Message *DP_message_deserialize_body(int type, unsigned int context_id, const unsigned char *buf, size_t length, bool decode_opaque) { - switch (type) { - {% for message in messages %} - case {{ message.enum_name }}: - {% if message.reserved %} - DP_error_set("Can't deserialize reserved message type {{ message.id }} {{ message.enum_name }}"); - return NULL; - {% else %} - return DP_{{ message.func_name }}_deserialize(context_id, buf, length); - {% endif %} - {% endfor %} - default: - DP_error_set("Can't deserialize unknown message type %d", type); - return NULL; + if (type < 64 || decode_opaque) { + switch (type) { + {% for message in messages %} + case {{ message.enum_name }}: + {% if message.reserved %} + DP_error_set("Can't deserialize reserved message type {{ message.id }} {{ message.enum_name }}"); + return NULL; + {% else %} + return DP_{{ message.func_name }}_deserialize(context_id, buf, length); + {% endif %} + {% endfor %} + default: + DP_error_set("Can't deserialize unknown message type %d", type); + return NULL; + } + } else { + return DP_message_new_opaque((DP_MessageType)type, context_id, buf, length); } } diff --git a/src/drawdance/generators/protogen/messages.h.jinja b/src/drawdance/generators/protogen/messages.h.jinja index 7e5f762c84..7f0667c04f 100644 --- a/src/drawdance/generators/protogen/messages.h.jinja +++ b/src/drawdance/generators/protogen/messages.h.jinja @@ -69,7 +69,7 @@ DP_MessageType DP_message_type_from_name(const char *type_name, DP_MessageType n bool DP_message_type_parse_multiline_tuples(DP_MessageType type); -DP_Message *DP_message_deserialize_body(int type, unsigned int context_id, const unsigned char *buf, size_t length); +DP_Message *DP_message_deserialize_body(int type, unsigned int context_id, const unsigned char *buf, size_t length, bool decode_opaque); DP_Message *DP_message_parse_body(DP_MessageType type, unsigned int context_id, DP_TextReader *reader); diff --git a/src/drawdance/libcommon/dpcommon/input.c b/src/drawdance/libcommon/dpcommon/input.c index 90bde679eb..9a83aa95a1 100644 --- a/src/drawdance/libcommon/dpcommon/input.c +++ b/src/drawdance/libcommon/dpcommon/input.c @@ -142,6 +142,19 @@ bool DP_input_seek(DP_Input *input, size_t offset) if (seek) { return seek(input->internal, offset); } + else { + DP_error_set("Seek not supported"); + return false; + } +} + +bool DP_input_seek_by(DP_Input *input, size_t size) +{ + DP_ASSERT(input); + bool (*seek_by)(void *, size_t) = input->methods->seek_by; + if (seek_by) { + return seek_by(input->internal, size); + } else { DP_error_set("Seek by not supported"); return false; @@ -216,8 +229,7 @@ static bool file_input_rewind(void *internal) static bool file_input_rewind_by(void *internal, size_t size) { DP_FileInputState *state = internal; - long offset = -DP_size_to_long(size); - if (fseek(state->fp, offset, SEEK_CUR) == 0) { + if (fseek(state->fp, -DP_size_to_long(size), SEEK_CUR) == 0) { return true; } else { @@ -240,6 +252,19 @@ static bool file_input_seek(void *internal, size_t offset) } } +static bool file_input_seek_by(void *internal, size_t size) +{ + DP_FileInputState *state = internal; + if (fseek(state->fp, DP_size_to_long(size), SEEK_CUR) == 0) { + return true; + } + else { + DP_error_set("File input could not seek by %zu: %s", size, + strerror(errno)); + return false; + } +} + static void file_input_dispose(void *internal) { DP_FileInputState *state = internal; @@ -250,7 +275,8 @@ static void file_input_dispose(void *internal) static const DP_InputMethods file_input_methods = { file_input_read, file_input_length, file_input_rewind, - file_input_rewind_by, file_input_seek, file_input_dispose, + file_input_rewind_by, file_input_seek, file_input_seek_by, + file_input_dispose, }; const DP_InputMethods *file_input_init(void *internal, void *arg) @@ -349,6 +375,12 @@ static bool mem_input_seek(void *internal, size_t offset) } } +static bool mem_input_seek_by(void *internal, size_t size) +{ + DP_MemInputState *state = internal; + return mem_input_seek(internal, state->pos + size); +} + static void mem_input_dispose(void *internal) { DP_MemInputState *state = internal; @@ -359,8 +391,8 @@ static void mem_input_dispose(void *internal) } static const DP_InputMethods mem_input_methods = { - mem_input_read, mem_input_length, mem_input_rewind, - mem_input_rewind_by, mem_input_seek, mem_input_dispose, + mem_input_read, mem_input_length, mem_input_rewind, mem_input_rewind_by, + mem_input_seek, mem_input_seek_by, mem_input_dispose, }; const DP_InputMethods *mem_input_init(void *internal, void *arg) diff --git a/src/drawdance/libcommon/dpcommon/input.h b/src/drawdance/libcommon/dpcommon/input.h index 0601da5a2d..3840ccfc50 100644 --- a/src/drawdance/libcommon/dpcommon/input.h +++ b/src/drawdance/libcommon/dpcommon/input.h @@ -33,6 +33,7 @@ typedef struct DP_InputMethods { bool (*rewind)(void *internal); bool (*rewind_by)(void *internal, size_t size); bool (*seek)(void *internal, size_t offset); + bool (*seek_by)(void *internal, size_t size); void (*dispose)(void *internal); } DP_InputMethods; @@ -53,6 +54,8 @@ bool DP_input_rewind_by(DP_Input *input, size_t size); bool DP_input_seek(DP_Input *input, size_t offset); +bool DP_input_seek_by(DP_Input *input, size_t size); + DP_Input *DP_file_input_new(FILE *fp, bool close); diff --git a/src/drawdance/libcommon/dpcommon/input_qt.cpp b/src/drawdance/libcommon/dpcommon/input_qt.cpp index 04efac2e07..0342c2af17 100644 --- a/src/drawdance/libcommon/dpcommon/input_qt.cpp +++ b/src/drawdance/libcommon/dpcommon/input_qt.cpp @@ -8,6 +8,7 @@ extern "C" { struct DP_QFileInputState { QFile *file; + bool close; }; QFile *get_file(void *internal) @@ -86,31 +87,57 @@ static bool qfile_input_seek(void *internal, size_t offset) } } -static void qfile_input_dispose(void *internal) +static bool qfile_input_seek_by(void *internal, size_t size) { QFile *file = get_file(internal); - file->close(); - delete file; + qint64 pos = file->pos(); + if (pos >= 0) { + qint64 target = pos + qint64(size); + if (target <= file->size() && file->seek(target)) { + return true; + } + } + DP_error_set("File input could not seek by %zu: %s", size, + qUtf8Printable(file->errorString())); + return false; +} + +static void qfile_input_dispose(void *internal) +{ + DP_QFileInputState *state = static_cast(internal); + if (state->close) { + QFile *file = state->file; + file->close(); + delete file; + } } static const DP_InputMethods qfile_input_methods = { qfile_input_read, qfile_input_length, qfile_input_rewind, - qfile_input_rewind_by, qfile_input_seek, qfile_input_dispose, + qfile_input_rewind_by, qfile_input_seek, qfile_input_seek_by, + qfile_input_dispose, }; const DP_InputMethods *qfile_input_init(void *internal, void *arg) { DP_QFileInputState *state = static_cast(internal); - state->file = static_cast(arg); + *state = *static_cast(arg); return &qfile_input_methods; } +extern "C" DP_Input *DP_qfile_input_new(QFile *file, bool close, + DP_InputQtNewFn new_fn) +{ + DP_QFileInputState state = {file, close}; + return new_fn(qfile_input_init, &state, sizeof(DP_QFileInputState)); +} + extern "C" DP_Input *DP_qfile_input_new_from_path(const char *path, DP_InputQtNewFn new_fn) { QFile *file = new QFile{QString::fromUtf8(path)}; if (file->open(QIODevice::ReadOnly)) { - return new_fn(qfile_input_init, file, sizeof(DP_QFileInputState)); + return DP_qfile_input_new(file, true, new_fn); } else { DP_error_set("Can't open '%s': %s", path, diff --git a/src/drawdance/libcommon/dpcommon/input_qt.h b/src/drawdance/libcommon/dpcommon/input_qt.h index cec8069c71..e1cb787fed 100644 --- a/src/drawdance/libcommon/dpcommon/input_qt.h +++ b/src/drawdance/libcommon/dpcommon/input_qt.h @@ -3,12 +3,20 @@ #include "common.h" #include "input.h" +#ifdef __cplusplus +class QFile; +#else +typedef struct QFile QFile; +#endif + // Need DP_input_new as a function pointer to avoid a circular dependency. typedef DP_Input *(*DP_InputQtNewFn)(DP_InputInitFn init, void *arg, size_t internal_size); +DP_Input *DP_qfile_input_new(QFile *file, bool close, DP_InputQtNewFn new_fn); + DP_Input *DP_qfile_input_new_from_path(const char *path, DP_InputQtNewFn new_fn); diff --git a/src/drawdance/libcommon/dpcommon/output_qt.cpp b/src/drawdance/libcommon/dpcommon/output_qt.cpp index acd773e068..5456e58784 100644 --- a/src/drawdance/libcommon/dpcommon/output_qt.cpp +++ b/src/drawdance/libcommon/dpcommon/output_qt.cpp @@ -63,6 +63,7 @@ static bool do_seek(const char *type, QIODevice *dev, size_t offset) struct DP_QFileOutputState { QFile *file; + bool close; }; static QFile *get_file(void *internal) @@ -93,31 +94,40 @@ static bool qfile_output_seek(void *internal, size_t offset) static bool qfile_output_dispose(void *internal) { - QFile *file = get_file(internal); + DP_QFileOutputState *state = static_cast(internal); + QFile *file = state->file; bool ok = do_flush("QFile", file); - delete file; + if (state->close) { + delete file; + } return ok; } static const DP_OutputMethods qfile_output_methods = { - qfile_output_write, nullptr, - qfile_output_flush, qfile_output_tell, - qfile_output_seek, qfile_output_dispose, + qfile_output_write, nullptr, qfile_output_flush, + qfile_output_tell, qfile_output_seek, qfile_output_dispose, }; static const DP_OutputMethods *qfile_output_init(void *internal, void *arg) { DP_QFileOutputState *state = static_cast(internal); - state->file = static_cast(arg); + *state = *static_cast(arg); return &qfile_output_methods; } +extern "C" DP_Output *DP_qfile_output_new(QFile *file, bool close, + DP_OutputQtNewFn new_fn) +{ + DP_QFileOutputState state = {file, close}; + return new_fn(qfile_output_init, &state, sizeof(DP_QFileOutputState)); +} + extern "C" DP_Output *DP_qfile_output_new_from_path(const char *path, DP_OutputQtNewFn new_fn) { QFile *file = new QFile{QString::fromUtf8(path)}; if (file->open(QIODevice::WriteOnly)) { - return new_fn(qfile_output_init, file, sizeof(DP_QFileOutputState)); + return DP_qfile_output_new(file, true, new_fn); } else { DP_error_set("Can't open '%s': %s", path, diff --git a/src/drawdance/libcommon/dpcommon/output_qt.h b/src/drawdance/libcommon/dpcommon/output_qt.h index bf63cbe176..777ff19959 100644 --- a/src/drawdance/libcommon/dpcommon/output_qt.h +++ b/src/drawdance/libcommon/dpcommon/output_qt.h @@ -3,12 +3,21 @@ #include "common.h" #include "output.h" +#ifdef __cplusplus +class QFile; +#else +typedef struct QFile QFile; +#endif + // Need DP_output_new as a function pointer to avoid a circular dependency. typedef DP_Output *(*DP_OutputQtNewFn)(DP_OutputInitFn init, void *arg, size_t internal_size); +DP_Output *DP_qfile_output_new(QFile *file, bool close, + DP_OutputQtNewFn new_fn); + DP_Output *DP_qfile_output_new_from_path(const char *path, DP_OutputQtNewFn new_fn); diff --git a/src/drawdance/libengine/dpengine/brush.h b/src/drawdance/libengine/dpengine/brush.h index 476a38de4e..1908df9736 100644 --- a/src/drawdance/libengine/dpengine/brush.h +++ b/src/drawdance/libengine/dpengine/brush.h @@ -24,11 +24,7 @@ #include "pixels.h" #include #include -#ifdef DP_BUNDLED_LIBMYPAINT -# include "libmypaint/mypaint-brush-settings-gen.h" -#else -# include -#endif +#include #define DP_CLASSIC_BRUSH_CURVE_VALUE_COUNT 256 #define DP_MYPAINT_CONTROL_POINTS_COUNT 64 diff --git a/src/drawdance/libengine/dpengine/dump_reader.c b/src/drawdance/libengine/dpengine/dump_reader.c index 2413061153..135fd9715a 100644 --- a/src/drawdance/libengine/dpengine/dump_reader.c +++ b/src/drawdance/libengine/dpengine/dump_reader.c @@ -94,7 +94,7 @@ static DP_DumpReaderResult handle_message(DP_DumpReader *dr) return DP_DUMP_READER_ERROR_INPUT; } - DP_Message *msg = DP_message_deserialize(dr->input.buffer, size); + DP_Message *msg = DP_message_deserialize(dr->input.buffer, size, true); if (msg) { DP_VECTOR_PUSH_TYPE(&dr->messages, DP_Message *, msg); return DP_DUMP_READER_SUCCESS; diff --git a/src/drawdance/libengine/dpengine/player.c b/src/drawdance/libengine/dpengine/player.c index ce156c0a5c..89e98afff5 100644 --- a/src/drawdance/libengine/dpengine/player.c +++ b/src/drawdance/libengine/dpengine/player.c @@ -209,7 +209,7 @@ static DP_Player *make_player(DP_PlayerType type, char *recording_path, static DP_Player *new_binary_player(char *recording_path, char *index_path, DP_Input *input) { - DP_BinaryReader *binary_reader = DP_binary_reader_new(input); + DP_BinaryReader *binary_reader = DP_binary_reader_new(input, 0); if (!binary_reader) { return NULL; } @@ -479,7 +479,7 @@ long long DP_player_position(DP_Player *player) static DP_PlayerResult step_binary(DP_Player *player, DP_Message **out_msg) { DP_BinaryReaderResult result = - DP_binary_reader_read_message(player->reader.binary, out_msg); + DP_binary_reader_read_message(player->reader.binary, true, out_msg); switch (result) { case DP_BINARY_READER_SUCCESS: return DP_PLAYER_SUCCESS; @@ -2487,7 +2487,7 @@ static bool read_index_history(DP_ReadSnapshotContext *c, size_t offset, } DP_Message *msg = DP_message_deserialize_length( - input->buffer, length, length < 2 ? 0 : length - 2); + input->buffer, length, length < 2 ? 0 : length - 2, true); if (msg) { snapshot->messages[i] = msg; } diff --git a/src/drawdance/libengine/dpengine/recorder.c b/src/drawdance/libengine/dpengine/recorder.c index cf44551b88..9d24183f32 100644 --- a/src/drawdance/libengine/dpengine/recorder.c +++ b/src/drawdance/libengine/dpengine/recorder.c @@ -146,7 +146,7 @@ static bool write_message_dec(DP_Recorder *r, DP_Message *msg) bool ok; switch (r->type) { case DP_RECORDER_TYPE_BINARY: - ok = DP_binary_writer_write_message(r->binary_writer, msg); + ok = DP_binary_writer_write_message(r->binary_writer, msg) != 0; break; case DP_RECORDER_TYPE_TEXT: ok = DP_message_write_text(msg, r->text_writer); diff --git a/src/drawdance/libmsg/dpmsg/binary_reader.c b/src/drawdance/libmsg/dpmsg/binary_reader.c index 58be9d9a95..2be4c41611 100644 --- a/src/drawdance/libmsg/dpmsg/binary_reader.c +++ b/src/drawdance/libmsg/dpmsg/binary_reader.c @@ -150,21 +150,34 @@ static JSON_Value *read_header(DP_Input *input, size_t *input_offset) } -DP_BinaryReader *DP_binary_reader_new(DP_Input *input) +DP_BinaryReader *DP_binary_reader_new(DP_Input *input, unsigned int flags) { DP_ASSERT(input); - bool error; - size_t input_length = DP_input_length(input, &error); - if (error) { - DP_input_free(input); - return NULL; + + size_t input_length; + if (flags & DP_BINARY_READER_FLAG_NO_LENGTH) { + input_length = 0; + } + else { + bool error; + input_length = DP_input_length(input, &error); + if (error) { + DP_input_free(input); + return NULL; + } } size_t input_offset = 0; - JSON_Value *header = read_header(input, &input_offset); - if (!header) { - DP_input_free(input); - return NULL; + JSON_Value *header; + if (flags & DP_BINARY_READER_FLAG_NO_HEADER) { + header = NULL; + } + else { + header = read_header(input, &input_offset); + if (!header) { + DP_input_free(input); + return NULL; + } } DP_BinaryReader *reader = DP_malloc(sizeof(*reader)); @@ -247,12 +260,9 @@ static size_t read_into(DP_BinaryReader *reader, size_t size, size_t offset, return read; } -DP_BinaryReaderResult DP_binary_reader_read_message(DP_BinaryReader *reader, - DP_Message **out_msg) +static DP_BinaryReaderResult read_message_header(DP_BinaryReader *reader, + size_t *out_body_length) { - DP_ASSERT(reader); - DP_ASSERT(out_msg); - bool error; size_t read = read_into(reader, DP_MESSAGE_HEADER_LENGTH, 0, &error); if (error) { @@ -266,20 +276,39 @@ DP_BinaryReaderResult DP_binary_reader_read_message(DP_BinaryReader *reader, DP_MESSAGE_HEADER_LENGTH, read); return DP_BINARY_READER_ERROR_INPUT; } + else { + *out_body_length = DP_read_bigendian_uint16(reader->buffer); + return DP_BINARY_READER_SUCCESS; + } +} - size_t body_length = DP_read_bigendian_uint16(reader->buffer); - read = read_into(reader, body_length, DP_MESSAGE_HEADER_LENGTH, &error); +DP_BinaryReaderResult DP_binary_reader_read_message(DP_BinaryReader *reader, + bool decode_opaque, + DP_Message **out_msg) +{ + DP_ASSERT(reader); + DP_ASSERT(out_msg); + + size_t body_length; + DP_BinaryReaderResult result = read_message_header(reader, &body_length); + if (result != DP_BINARY_READER_SUCCESS) { + return result; + } + + bool error; + size_t read = + read_into(reader, body_length, DP_MESSAGE_HEADER_LENGTH, &error); if (error) { return DP_BINARY_READER_ERROR_INPUT; } else if (read != body_length) { - DP_error_set("Treid to read message body of %zu bytes, but got %zu", + DP_error_set("Tried to read message body of %zu bytes, but got %zu", body_length, read); return DP_BINARY_READER_ERROR_INPUT; } DP_Message *msg = DP_message_deserialize( - reader->buffer, DP_MESSAGE_HEADER_LENGTH + body_length); + reader->buffer, DP_MESSAGE_HEADER_LENGTH + body_length, decode_opaque); if (msg) { *out_msg = msg; return DP_BINARY_READER_SUCCESS; @@ -288,3 +317,28 @@ DP_BinaryReaderResult DP_binary_reader_read_message(DP_BinaryReader *reader, return DP_BINARY_READER_ERROR_PARSE; } } + +int DP_binary_reader_skip_message(DP_BinaryReader *reader, uint8_t *out_type, + uint8_t *out_context_id) +{ + DP_ASSERT(reader); + + size_t body_length; + DP_BinaryReaderResult result = read_message_header(reader, &body_length); + if (result != DP_BINARY_READER_SUCCESS) { + return -1; + } + + DP_Input *input = reader->input; + if (!DP_input_seek_by(input, body_length)) { + return -1; + } + + if (out_type) { + *out_type = reader->buffer[2]; + } + if (out_context_id) { + *out_context_id = reader->buffer[3]; + } + return DP_size_to_int(DP_MESSAGE_HEADER_LENGTH + body_length); +} diff --git a/src/drawdance/libmsg/dpmsg/binary_reader.h b/src/drawdance/libmsg/dpmsg/binary_reader.h index 54eb00225a..2e88e78236 100644 --- a/src/drawdance/libmsg/dpmsg/binary_reader.h +++ b/src/drawdance/libmsg/dpmsg/binary_reader.h @@ -30,6 +30,9 @@ typedef struct json_value_t JSON_Value; #define DP_DPREC_MAGIC "DPREC" #define DP_DPREC_MAGIC_LENGTH 6 +#define DP_BINARY_READER_FLAG_NO_LENGTH 0x1u +#define DP_BINARY_READER_FLAG_NO_HEADER 0x2u + typedef struct DP_BinaryReader DP_BinaryReader; typedef enum DP_BinaryReaderResult { @@ -39,7 +42,7 @@ typedef enum DP_BinaryReaderResult { DP_BINARY_READER_ERROR_PARSE, } DP_BinaryReaderResult; -DP_BinaryReader *DP_binary_reader_new(DP_Input *input); +DP_BinaryReader *DP_binary_reader_new(DP_Input *input, unsigned int flags); void DP_binary_reader_free(DP_BinaryReader *reader); @@ -55,7 +58,12 @@ bool DP_binary_reader_seek(DP_BinaryReader *reader, size_t offset); double DP_binary_reader_progress(DP_BinaryReader *reader); DP_BinaryReaderResult DP_binary_reader_read_message(DP_BinaryReader *reader, + bool decode_opaque, DP_Message **out_msg); +// Returns the message length (including header) or -1 on error. +int DP_binary_reader_skip_message(DP_BinaryReader *reader, uint8_t *out_type, + uint8_t *out_context_id); + #endif diff --git a/src/drawdance/libmsg/dpmsg/binary_writer.c b/src/drawdance/libmsg/dpmsg/binary_writer.c index 905e72bd59..758bd9073e 100644 --- a/src/drawdance/libmsg/dpmsg/binary_writer.c +++ b/src/drawdance/libmsg/dpmsg/binary_writer.c @@ -107,15 +107,13 @@ static unsigned char *get_buffer(void *user, size_t size) return reserve(user, size); } -bool DP_binary_writer_write_message(DP_BinaryWriter *writer, DP_Message *msg) +size_t DP_binary_writer_write_message(DP_BinaryWriter *writer, DP_Message *msg) { DP_ASSERT(writer); DP_ASSERT(msg); - size_t length = DP_message_serialize(msg, true, get_buffer, writer); - if (length == 0) { - return false; - } - - return DP_output_write(writer->output, writer->buffer, length); + return length != 0 + && DP_output_write(writer->output, writer->buffer, length) + ? length + : 0; } diff --git a/src/drawdance/libmsg/dpmsg/binary_writer.h b/src/drawdance/libmsg/dpmsg/binary_writer.h index 79dcb3e927..cfa21c6f97 100644 --- a/src/drawdance/libmsg/dpmsg/binary_writer.h +++ b/src/drawdance/libmsg/dpmsg/binary_writer.h @@ -22,11 +22,7 @@ #ifndef DPMSG_BINARY_WRITER_H #define DPMSG_BINARY_WRITER_H #include -#ifdef DP_BUNDLED_PARSON -#include "parson/parson.h" -#else #include -#endif typedef struct DP_Message DP_Message; typedef struct DP_Output DP_Output; @@ -43,8 +39,9 @@ bool DP_binary_writer_write_header(DP_BinaryWriter *writer, JSON_Object *header) DP_MUST_CHECK; -bool DP_binary_writer_write_message(DP_BinaryWriter *writer, - DP_Message *msg) DP_MUST_CHECK; +// Returns the length of the written message (including header) or 0 on error. +size_t DP_binary_writer_write_message(DP_BinaryWriter *writer, + DP_Message *msg) DP_MUST_CHECK; #endif diff --git a/src/drawdance/libmsg/dpmsg/message.c b/src/drawdance/libmsg/dpmsg/message.c index 8c4149edbe..e05b8efd85 100644 --- a/src/drawdance/libmsg/dpmsg/message.c +++ b/src/drawdance/libmsg/dpmsg/message.c @@ -25,8 +25,9 @@ #include #include -#define COMPAT_FLAG_NONE 0x0 -#define COMPAT_FLAG_INDIRECT 0x1 +#define FLAG_NONE 0x0 +#define FLAG_OPAQUE 0x1 +#define FLAG_COMPAT_INDIRECT 0x2 typedef DP_Message *(*DP_MessageDeserializeFn)(unsigned int context_id, const unsigned char *buffer, @@ -35,7 +36,7 @@ typedef DP_Message *(*DP_MessageDeserializeFn)(unsigned int context_id, struct DP_Message { DP_Atomic refcount; uint8_t type; - uint8_t compat_flags; + uint8_t flags; unsigned int context_id; const DP_MessageMethods *methods; alignas(DP_max_align_t) unsigned char internal[]; @@ -54,15 +55,81 @@ DP_Message *DP_message_new(DP_MessageType type, unsigned int context_id, DP_ASSERT(methods->equals); DP_ASSERT(methods->write_payload_text); DP_ASSERT(internal_size <= SIZE_MAX - sizeof(DP_Message)); - DP_Message *msg = DP_malloc_zeroed(sizeof(*msg) + internal_size); + DP_Message *msg = + DP_malloc_zeroed(DP_FLEX_SIZEOF(DP_Message, internal, internal_size)); DP_atomic_set(&msg->refcount, 1); msg->type = (uint8_t)type; - msg->compat_flags = COMPAT_FLAG_NONE; + msg->flags = FLAG_NONE; msg->context_id = context_id; msg->methods = methods; return msg; } +typedef struct DP_OpaqueMessage { + size_t length; + unsigned char body[]; +} DP_OpaqueMessage; + +static size_t opaque_payload_length(DP_Message *msg) +{ + DP_OpaqueMessage *om = (void *)msg->internal; + return om->length; +} + +static size_t opaque_serialize_payload(DP_Message *msg, unsigned char *data) +{ + DP_OpaqueMessage *om = (void *)msg->internal; + size_t length = om->length; + if (length != 0) { + memcpy(data, om->body, length); + } + return length; +} + +static bool opaque_write_payload_text(DP_UNUSED DP_Message *msg, + DP_UNUSED DP_TextWriter *writer) +{ + DP_error_set("Can't write payload text of opaque message"); + return false; +} + +static bool opaque_equals(DP_Message *DP_RESTRICT msg, + DP_Message *DP_RESTRICT other) +{ + DP_OpaqueMessage *a = (void *)msg->internal; + DP_OpaqueMessage *b = (void *)other->internal; + return a->length == b->length && memcmp(a->body, b->body, a->length) == 0; +} + +static const DP_MessageMethods opaque_methods = { + opaque_payload_length, + opaque_serialize_payload, + opaque_write_payload_text, + opaque_equals, +}; + +DP_Message *DP_message_new_opaque(DP_MessageType type, unsigned int context_id, + const unsigned char *body, size_t length) +{ + DP_ASSERT(type >= 0); + DP_ASSERT(type <= DP_MESSAGE_MAX); + DP_ASSERT(context_id <= UINT8_MAX); + DP_ASSERT(length <= SIZE_MAX - sizeof(DP_Message)); + DP_Message *msg = DP_malloc_zeroed(DP_FLEX_SIZEOF( + DP_Message, internal, DP_FLEX_SIZEOF(DP_OpaqueMessage, body, length))); + DP_atomic_set(&msg->refcount, 1); + msg->type = (uint8_t)type; + msg->flags = FLAG_OPAQUE; + msg->context_id = context_id; + msg->methods = &opaque_methods; + DP_OpaqueMessage *om = (void *)msg->internal; + om->length = length; + if (length != 0) { + memcpy(om->body, body, length); + } + return msg; +} + DP_Message *DP_message_incref(DP_Message *msg) { DP_ASSERT(msg); @@ -108,6 +175,13 @@ DP_MessageType DP_message_type(DP_Message *msg) return (DP_MessageType)msg->type; } +bool DP_message_opaque(DP_Message *msg) +{ + DP_ASSERT(msg); + DP_ASSERT(DP_atomic_get(&msg->refcount) > 0); + return msg->flags & FLAG_OPAQUE; +} + const char *DP_message_name(DP_Message *msg) { return DP_message_type_name(DP_message_type(msg)); @@ -131,6 +205,7 @@ void *DP_message_internal(DP_Message *msg) { DP_ASSERT(msg); DP_ASSERT(DP_atomic_get(&msg->refcount) > 0); + DP_ASSERT(!DP_message_opaque(msg)); return msg->internal; } @@ -155,38 +230,6 @@ void *DP_message_cast(DP_Message *msg, DP_MessageType type) return NULL; } -void *DP_message_cast2(DP_Message *msg, DP_MessageType type1, - DP_MessageType type2) -{ - if (msg) { - DP_MessageType msg_type = (DP_MessageType)msg->type; - if (msg_type == type1 || msg_type == type2) { - return DP_message_internal(msg); - } - else { - DP_error_set("Wrong message type %d, wanted %d or %d", msg_type, - type1, type2); - } - } - return NULL; -} - -void *DP_message_cast3(DP_Message *msg, DP_MessageType type1, - DP_MessageType type2, DP_MessageType type3) -{ - if (msg) { - DP_MessageType msg_type = (DP_MessageType)msg->type; - if (msg_type == type1 || msg_type == type2 || msg_type == type3) { - return DP_message_internal(msg); - } - else { - DP_error_set("Wrong message type %d, wanted %d, %d or %d", msg_type, - type1, type2, type3); - } - } - return NULL; -} - size_t DP_message_length(DP_Message *msg) { DP_ASSERT(msg); @@ -250,20 +293,28 @@ bool DP_message_write_text(DP_Message *msg, DP_TextWriter *writer) bool DP_message_equals(DP_Message *msg, DP_Message *other) { - return (msg == other) - || (msg && other && msg->type == other->type - && msg->methods->equals(msg, other)); + if (msg == other) { + return true; + } + else if (msg && other && msg->type == other->type) { + DP_ASSERT(DP_message_opaque(msg) == DP_message_opaque(other)); + return msg->methods->equals(msg, other); + } + else { + return false; + } } DP_Message *DP_message_deserialize_length(const unsigned char *buf, - size_t bufsize, size_t body_length) + size_t bufsize, size_t body_length, + bool decode_opaque) { DP_ASSERT(buf); size_t total_length = 2 + body_length; if (bufsize >= total_length) { - return DP_message_deserialize_body(buf[0], buf[1], buf + 2, - body_length); + return DP_message_deserialize_body(buf[0], buf[1], buf + 2, body_length, + decode_opaque); } else { DP_error_set("Buffer size %zu shorter than message length %zu", bufsize, @@ -272,12 +323,14 @@ DP_Message *DP_message_deserialize_length(const unsigned char *buf, } } -DP_Message *DP_message_deserialize(const unsigned char *buf, size_t bufsize) +DP_Message *DP_message_deserialize(const unsigned char *buf, size_t bufsize, + bool decode_opaque) { if (bufsize >= DP_MESSAGE_HEADER_LENGTH) { DP_ASSERT(buf); size_t body_length = DP_read_bigendian_uint16(buf); - return DP_message_deserialize_length(buf + 2, bufsize - 2, body_length); + return DP_message_deserialize_length(buf + 2, bufsize - 2, body_length, + decode_opaque); } else { DP_error_set("Buffer size %zu too short for message header", bufsize); @@ -290,14 +343,14 @@ bool DP_message_compat_flag_indirect(DP_Message *msg) { DP_ASSERT(msg); DP_ASSERT(DP_atomic_get(&msg->refcount) > 0); - return msg->compat_flags & COMPAT_FLAG_INDIRECT; + return msg->flags & FLAG_COMPAT_INDIRECT; } void DP_message_compat_flag_indirect_set(DP_Message *msg) { DP_ASSERT(msg); DP_ASSERT(DP_atomic_get(&msg->refcount) > 0); - msg->compat_flags |= COMPAT_FLAG_INDIRECT; + msg->flags |= FLAG_COMPAT_INDIRECT; } diff --git a/src/drawdance/libmsg/dpmsg/message.h b/src/drawdance/libmsg/dpmsg/message.h index b8024bf2a6..db7a34626d 100644 --- a/src/drawdance/libmsg/dpmsg/message.h +++ b/src/drawdance/libmsg/dpmsg/message.h @@ -40,6 +40,9 @@ DP_Message *DP_message_new(DP_MessageType type, unsigned int context_id, const DP_MessageMethods *methods, size_t internal_size); +DP_Message *DP_message_new_opaque(DP_MessageType type, unsigned int context_id, + const unsigned char *body, size_t length); + DP_Message *DP_message_incref(DP_Message *msg); DP_Message *DP_message_incref_nullable(DP_Message *msg_or_null); @@ -53,6 +56,8 @@ int DP_message_refcount(DP_Message *msg); DP_MessageType DP_message_type(DP_Message *msg); +bool DP_message_opaque(DP_Message *msg); + const char *DP_message_name(DP_Message *msg); unsigned int DP_message_context_id(DP_Message *msg); @@ -65,12 +70,6 @@ DP_Message *DP_message_from_internal(void *internal); void *DP_message_cast(DP_Message *msg, DP_MessageType type); -void *DP_message_cast2(DP_Message *msg, DP_MessageType type1, - DP_MessageType type2); - -void *DP_message_cast3(DP_Message *msg, DP_MessageType type1, - DP_MessageType type2, DP_MessageType type3); - size_t DP_message_length(DP_Message *msg); size_t DP_message_serialize(DP_Message *msg, bool write_body_length, @@ -84,9 +83,11 @@ bool DP_message_equals(DP_Message *msg, DP_Message *other); DP_Message *DP_message_deserialize_length(const unsigned char *buf, - size_t bufsize, size_t body_length); + size_t bufsize, size_t body_length, + bool decode_opaque); -DP_Message *DP_message_deserialize(const unsigned char *buf, size_t bufsize); +DP_Message *DP_message_deserialize(const unsigned char *buf, size_t bufsize, + bool decode_opaque); bool DP_message_compat_flag_indirect(DP_Message *msg); diff --git a/src/drawdance/libmsg/dpmsg/messages.c b/src/drawdance/libmsg/dpmsg/messages.c index de0bf9cb48..d1ff1c8aa9 100644 --- a/src/drawdance/libmsg/dpmsg/messages.c +++ b/src/drawdance/libmsg/dpmsg/messages.c @@ -601,149 +601,165 @@ bool DP_message_type_parse_multiline_tuples(DP_MessageType type) DP_Message *DP_message_deserialize_body(int type, unsigned int context_id, - const unsigned char *buf, size_t length) -{ - switch (type) { - case DP_MSG_SERVER_COMMAND: - return DP_msg_server_command_deserialize(context_id, buf, length); - case DP_MSG_DISCONNECT: - return DP_msg_disconnect_deserialize(context_id, buf, length); - case DP_MSG_PING: - return DP_msg_ping_deserialize(context_id, buf, length); - case DP_MSG_INTERNAL: - DP_error_set( - "Can't deserialize reserved message type 31 DP_MSG_INTERNAL"); - return NULL; - case DP_MSG_JOIN: - return DP_msg_join_deserialize(context_id, buf, length); - case DP_MSG_LEAVE: - return DP_msg_leave_deserialize(context_id, buf, length); - case DP_MSG_SESSION_OWNER: - return DP_msg_session_owner_deserialize(context_id, buf, length); - case DP_MSG_CHAT: - return DP_msg_chat_deserialize(context_id, buf, length); - case DP_MSG_TRUSTED_USERS: - return DP_msg_trusted_users_deserialize(context_id, buf, length); - case DP_MSG_SOFT_RESET: - return DP_msg_soft_reset_deserialize(context_id, buf, length); - case DP_MSG_PRIVATE_CHAT: - return DP_msg_private_chat_deserialize(context_id, buf, length); - case DP_MSG_INTERVAL: - return DP_msg_interval_deserialize(context_id, buf, length); - case DP_MSG_LASER_TRAIL: - return DP_msg_laser_trail_deserialize(context_id, buf, length); - case DP_MSG_MOVE_POINTER: - return DP_msg_move_pointer_deserialize(context_id, buf, length); - case DP_MSG_MARKER: - return DP_msg_marker_deserialize(context_id, buf, length); - case DP_MSG_USER_ACL: - return DP_msg_user_acl_deserialize(context_id, buf, length); - case DP_MSG_LAYER_ACL: - return DP_msg_layer_acl_deserialize(context_id, buf, length); - case DP_MSG_FEATURE_ACCESS_LEVELS: - return DP_msg_feature_access_levels_deserialize(context_id, buf, + const unsigned char *buf, size_t length, + bool decode_opaque) +{ + if (type < 64 || decode_opaque) { + switch (type) { + case DP_MSG_SERVER_COMMAND: + return DP_msg_server_command_deserialize(context_id, buf, length); + case DP_MSG_DISCONNECT: + return DP_msg_disconnect_deserialize(context_id, buf, length); + case DP_MSG_PING: + return DP_msg_ping_deserialize(context_id, buf, length); + case DP_MSG_INTERNAL: + DP_error_set( + "Can't deserialize reserved message type 31 DP_MSG_INTERNAL"); + return NULL; + case DP_MSG_JOIN: + return DP_msg_join_deserialize(context_id, buf, length); + case DP_MSG_LEAVE: + return DP_msg_leave_deserialize(context_id, buf, length); + case DP_MSG_SESSION_OWNER: + return DP_msg_session_owner_deserialize(context_id, buf, length); + case DP_MSG_CHAT: + return DP_msg_chat_deserialize(context_id, buf, length); + case DP_MSG_TRUSTED_USERS: + return DP_msg_trusted_users_deserialize(context_id, buf, length); + case DP_MSG_SOFT_RESET: + return DP_msg_soft_reset_deserialize(context_id, buf, length); + case DP_MSG_PRIVATE_CHAT: + return DP_msg_private_chat_deserialize(context_id, buf, length); + case DP_MSG_INTERVAL: + return DP_msg_interval_deserialize(context_id, buf, length); + case DP_MSG_LASER_TRAIL: + return DP_msg_laser_trail_deserialize(context_id, buf, length); + case DP_MSG_MOVE_POINTER: + return DP_msg_move_pointer_deserialize(context_id, buf, length); + case DP_MSG_MARKER: + return DP_msg_marker_deserialize(context_id, buf, length); + case DP_MSG_USER_ACL: + return DP_msg_user_acl_deserialize(context_id, buf, length); + case DP_MSG_LAYER_ACL: + return DP_msg_layer_acl_deserialize(context_id, buf, length); + case DP_MSG_FEATURE_ACCESS_LEVELS: + return DP_msg_feature_access_levels_deserialize(context_id, buf, + length); + case DP_MSG_DEFAULT_LAYER: + return DP_msg_default_layer_deserialize(context_id, buf, length); + case DP_MSG_FILTERED: + return DP_msg_filtered_deserialize(context_id, buf, length); + case DP_MSG_EXTENSION: + DP_error_set( + "Can't deserialize reserved message type 73 DP_MSG_EXTENSION"); + return NULL; + case DP_MSG_UNDO_DEPTH: + return DP_msg_undo_depth_deserialize(context_id, buf, length); + case DP_MSG_DATA: + return DP_msg_data_deserialize(context_id, buf, length); + case DP_MSG_LOCAL_CHANGE: + return DP_msg_local_change_deserialize(context_id, buf, length); + case DP_MSG_UNDO_POINT: + return DP_msg_undo_point_deserialize(context_id, buf, length); + case DP_MSG_CANVAS_RESIZE: + return DP_msg_canvas_resize_deserialize(context_id, buf, length); + case DP_MSG_LAYER_CREATE: + return DP_msg_layer_create_deserialize(context_id, buf, length); + case DP_MSG_LAYER_ATTRIBUTES: + return DP_msg_layer_attributes_deserialize(context_id, buf, length); + case DP_MSG_LAYER_RETITLE: + return DP_msg_layer_retitle_deserialize(context_id, buf, length); + case DP_MSG_LAYER_ORDER: + return DP_msg_layer_order_deserialize(context_id, buf, length); + case DP_MSG_LAYER_DELETE: + return DP_msg_layer_delete_deserialize(context_id, buf, length); + case DP_MSG_LAYER_VISIBILITY: + return DP_msg_layer_visibility_deserialize(context_id, buf, length); + case DP_MSG_PUT_IMAGE: + return DP_msg_put_image_deserialize(context_id, buf, length); + case DP_MSG_FILL_RECT: + return DP_msg_fill_rect_deserialize(context_id, buf, length); + case DP_MSG_TOOL_CHANGE: + DP_error_set("Can't deserialize reserved message type 138 " + "DP_MSG_TOOL_CHANGE"); + return NULL; + case DP_MSG_PEN_MOVE: + DP_error_set( + "Can't deserialize reserved message type 139 DP_MSG_PEN_MOVE"); + return NULL; + case DP_MSG_PEN_UP: + return DP_msg_pen_up_deserialize(context_id, buf, length); + case DP_MSG_ANNOTATION_CREATE: + return DP_msg_annotation_create_deserialize(context_id, buf, length); - case DP_MSG_DEFAULT_LAYER: - return DP_msg_default_layer_deserialize(context_id, buf, length); - case DP_MSG_FILTERED: - return DP_msg_filtered_deserialize(context_id, buf, length); - case DP_MSG_EXTENSION: - DP_error_set( - "Can't deserialize reserved message type 73 DP_MSG_EXTENSION"); - return NULL; - case DP_MSG_UNDO_DEPTH: - return DP_msg_undo_depth_deserialize(context_id, buf, length); - case DP_MSG_DATA: - return DP_msg_data_deserialize(context_id, buf, length); - case DP_MSG_LOCAL_CHANGE: - return DP_msg_local_change_deserialize(context_id, buf, length); - case DP_MSG_UNDO_POINT: - return DP_msg_undo_point_deserialize(context_id, buf, length); - case DP_MSG_CANVAS_RESIZE: - return DP_msg_canvas_resize_deserialize(context_id, buf, length); - case DP_MSG_LAYER_CREATE: - return DP_msg_layer_create_deserialize(context_id, buf, length); - case DP_MSG_LAYER_ATTRIBUTES: - return DP_msg_layer_attributes_deserialize(context_id, buf, length); - case DP_MSG_LAYER_RETITLE: - return DP_msg_layer_retitle_deserialize(context_id, buf, length); - case DP_MSG_LAYER_ORDER: - return DP_msg_layer_order_deserialize(context_id, buf, length); - case DP_MSG_LAYER_DELETE: - return DP_msg_layer_delete_deserialize(context_id, buf, length); - case DP_MSG_LAYER_VISIBILITY: - return DP_msg_layer_visibility_deserialize(context_id, buf, length); - case DP_MSG_PUT_IMAGE: - return DP_msg_put_image_deserialize(context_id, buf, length); - case DP_MSG_FILL_RECT: - return DP_msg_fill_rect_deserialize(context_id, buf, length); - case DP_MSG_TOOL_CHANGE: - DP_error_set( - "Can't deserialize reserved message type 138 DP_MSG_TOOL_CHANGE"); - return NULL; - case DP_MSG_PEN_MOVE: - DP_error_set( - "Can't deserialize reserved message type 139 DP_MSG_PEN_MOVE"); - return NULL; - case DP_MSG_PEN_UP: - return DP_msg_pen_up_deserialize(context_id, buf, length); - case DP_MSG_ANNOTATION_CREATE: - return DP_msg_annotation_create_deserialize(context_id, buf, length); - case DP_MSG_ANNOTATION_RESHAPE: - return DP_msg_annotation_reshape_deserialize(context_id, buf, length); - case DP_MSG_ANNOTATION_EDIT: - return DP_msg_annotation_edit_deserialize(context_id, buf, length); - case DP_MSG_ANNOTATION_DELETE: - return DP_msg_annotation_delete_deserialize(context_id, buf, length); - case DP_MSG_MOVE_REGION: - return DP_msg_move_region_deserialize(context_id, buf, length); - case DP_MSG_PUT_TILE: - return DP_msg_put_tile_deserialize(context_id, buf, length); - case DP_MSG_CANVAS_BACKGROUND: - return DP_msg_canvas_background_deserialize(context_id, buf, length); - case DP_MSG_DRAW_DABS_CLASSIC: - return DP_msg_draw_dabs_classic_deserialize(context_id, buf, length); - case DP_MSG_DRAW_DABS_PIXEL: - return DP_msg_draw_dabs_pixel_deserialize(context_id, buf, length); - case DP_MSG_DRAW_DABS_PIXEL_SQUARE: - return DP_msg_draw_dabs_pixel_square_deserialize(context_id, buf, + case DP_MSG_ANNOTATION_RESHAPE: + return DP_msg_annotation_reshape_deserialize(context_id, buf, length); - case DP_MSG_DRAW_DABS_MYPAINT: - return DP_msg_draw_dabs_mypaint_deserialize(context_id, buf, length); - case DP_MSG_MOVE_RECT: - return DP_msg_move_rect_deserialize(context_id, buf, length); - case DP_MSG_SET_METADATA_INT: - return DP_msg_set_metadata_int_deserialize(context_id, buf, length); - case DP_MSG_LAYER_TREE_CREATE: - return DP_msg_layer_tree_create_deserialize(context_id, buf, length); - case DP_MSG_LAYER_TREE_MOVE: - return DP_msg_layer_tree_move_deserialize(context_id, buf, length); - case DP_MSG_LAYER_TREE_DELETE: - return DP_msg_layer_tree_delete_deserialize(context_id, buf, length); - case DP_MSG_TRANSFORM_REGION: - return DP_msg_transform_region_deserialize(context_id, buf, length); - case DP_MSG_TRACK_CREATE: - return DP_msg_track_create_deserialize(context_id, buf, length); - case DP_MSG_TRACK_RETITLE: - return DP_msg_track_retitle_deserialize(context_id, buf, length); - case DP_MSG_TRACK_DELETE: - return DP_msg_track_delete_deserialize(context_id, buf, length); - case DP_MSG_TRACK_ORDER: - return DP_msg_track_order_deserialize(context_id, buf, length); - case DP_MSG_KEY_FRAME_SET: - return DP_msg_key_frame_set_deserialize(context_id, buf, length); - case DP_MSG_KEY_FRAME_RETITLE: - return DP_msg_key_frame_retitle_deserialize(context_id, buf, length); - case DP_MSG_KEY_FRAME_LAYER_ATTRIBUTES: - return DP_msg_key_frame_layer_attributes_deserialize(context_id, buf, + case DP_MSG_ANNOTATION_EDIT: + return DP_msg_annotation_edit_deserialize(context_id, buf, length); + case DP_MSG_ANNOTATION_DELETE: + return DP_msg_annotation_delete_deserialize(context_id, buf, + length); + case DP_MSG_MOVE_REGION: + return DP_msg_move_region_deserialize(context_id, buf, length); + case DP_MSG_PUT_TILE: + return DP_msg_put_tile_deserialize(context_id, buf, length); + case DP_MSG_CANVAS_BACKGROUND: + return DP_msg_canvas_background_deserialize(context_id, buf, + length); + case DP_MSG_DRAW_DABS_CLASSIC: + return DP_msg_draw_dabs_classic_deserialize(context_id, buf, + length); + case DP_MSG_DRAW_DABS_PIXEL: + return DP_msg_draw_dabs_pixel_deserialize(context_id, buf, length); + case DP_MSG_DRAW_DABS_PIXEL_SQUARE: + return DP_msg_draw_dabs_pixel_square_deserialize(context_id, buf, length); - case DP_MSG_KEY_FRAME_DELETE: - return DP_msg_key_frame_delete_deserialize(context_id, buf, length); - case DP_MSG_UNDO: - return DP_msg_undo_deserialize(context_id, buf, length); - default: - DP_error_set("Can't deserialize unknown message type %d", type); - return NULL; + case DP_MSG_DRAW_DABS_MYPAINT: + return DP_msg_draw_dabs_mypaint_deserialize(context_id, buf, + length); + case DP_MSG_MOVE_RECT: + return DP_msg_move_rect_deserialize(context_id, buf, length); + case DP_MSG_SET_METADATA_INT: + return DP_msg_set_metadata_int_deserialize(context_id, buf, length); + case DP_MSG_LAYER_TREE_CREATE: + return DP_msg_layer_tree_create_deserialize(context_id, buf, + length); + case DP_MSG_LAYER_TREE_MOVE: + return DP_msg_layer_tree_move_deserialize(context_id, buf, length); + case DP_MSG_LAYER_TREE_DELETE: + return DP_msg_layer_tree_delete_deserialize(context_id, buf, + length); + case DP_MSG_TRANSFORM_REGION: + return DP_msg_transform_region_deserialize(context_id, buf, length); + case DP_MSG_TRACK_CREATE: + return DP_msg_track_create_deserialize(context_id, buf, length); + case DP_MSG_TRACK_RETITLE: + return DP_msg_track_retitle_deserialize(context_id, buf, length); + case DP_MSG_TRACK_DELETE: + return DP_msg_track_delete_deserialize(context_id, buf, length); + case DP_MSG_TRACK_ORDER: + return DP_msg_track_order_deserialize(context_id, buf, length); + case DP_MSG_KEY_FRAME_SET: + return DP_msg_key_frame_set_deserialize(context_id, buf, length); + case DP_MSG_KEY_FRAME_RETITLE: + return DP_msg_key_frame_retitle_deserialize(context_id, buf, + length); + case DP_MSG_KEY_FRAME_LAYER_ATTRIBUTES: + return DP_msg_key_frame_layer_attributes_deserialize(context_id, + buf, length); + case DP_MSG_KEY_FRAME_DELETE: + return DP_msg_key_frame_delete_deserialize(context_id, buf, length); + case DP_MSG_UNDO: + return DP_msg_undo_deserialize(context_id, buf, length); + default: + DP_error_set("Can't deserialize unknown message type %d", type); + return NULL; + } + } + else { + return DP_message_new_opaque((DP_MessageType)type, context_id, buf, + length); } } diff --git a/src/drawdance/libmsg/dpmsg/messages.h b/src/drawdance/libmsg/dpmsg/messages.h index 55d7bd64e2..703b054940 100644 --- a/src/drawdance/libmsg/dpmsg/messages.h +++ b/src/drawdance/libmsg/dpmsg/messages.h @@ -131,8 +131,8 @@ bool DP_message_type_parse_multiline_tuples(DP_MessageType type); DP_Message *DP_message_deserialize_body(int type, unsigned int context_id, - const unsigned char *buf, - size_t length); + const unsigned char *buf, size_t length, + bool decode_opaque); DP_Message *DP_message_parse_body(DP_MessageType type, unsigned int context_id, DP_TextReader *reader); diff --git a/src/drawdance/libmsg/dpmsg/msg_internal.c b/src/drawdance/libmsg/dpmsg/msg_internal.c index 6fe03a43df..7b77fa1646 100644 --- a/src/drawdance/libmsg/dpmsg/msg_internal.c +++ b/src/drawdance/libmsg/dpmsg/msg_internal.c @@ -221,6 +221,12 @@ DP_Message *DP_msg_internal_local_fork_clear_new(unsigned int context_id) sizeof(DP_MsgInternal)); } +DP_Message *DP_msg_internal_flush_new(unsigned int context_id) +{ + return msg_internal_new(context_id, DP_MSG_INTERNAL_TYPE_FLUSH, + sizeof(DP_MsgInternal)); +} + DP_MsgInternal *DP_msg_internal_cast(DP_Message *msg) { diff --git a/src/drawdance/libmsg/dpmsg/msg_internal.h b/src/drawdance/libmsg/dpmsg/msg_internal.h index dc075cb8a2..aecfbe43a1 100644 --- a/src/drawdance/libmsg/dpmsg/msg_internal.h +++ b/src/drawdance/libmsg/dpmsg/msg_internal.h @@ -38,6 +38,7 @@ typedef enum DP_MsgInternalType { DP_MSG_INTERNAL_TYPE_DUMP_PLAYBACK, DP_MSG_INTERNAL_TYPE_DUMP_COMMAND, DP_MSG_INTERNAL_TYPE_LOCAL_FORK_CLEAR, + DP_MSG_INTERNAL_TYPE_FLUSH, DP_MSG_INTERNAL_TYPE_COUNT, } DP_MsgInternalType; @@ -74,6 +75,8 @@ DP_Message *DP_msg_internal_dump_command_new_inc(unsigned int context_id, DP_Message *DP_msg_internal_local_fork_clear_new(unsigned int context_id); +DP_Message *DP_msg_internal_flush_new(unsigned int context_id); + DP_MsgInternal *DP_msg_internal_cast(DP_Message *msg); diff --git a/src/drawdance/libmsg/dpmsg/protover.rs b/src/drawdance/libmsg/dpmsg/protover.rs index 4e5bb0d262..bccf502752 100644 --- a/src/drawdance/libmsg/dpmsg/protover.rs +++ b/src/drawdance/libmsg/dpmsg/protover.rs @@ -96,13 +96,17 @@ impl ProtocolVersion { #[no_mangle] pub extern "C" fn DP_protocol_version_parse(s: *const c_char) -> *mut ProtocolVersion { - match unsafe { CStr::from_ptr(s) } - .to_str() - .ok() - .and_then(ProtocolVersion::parse) - { - Some(protover) => Box::into_raw(Box::new(protover)), - None => null_mut(), + if s.is_null() { + null_mut() + } else { + match unsafe { CStr::from_ptr(s) } + .to_str() + .ok() + .and_then(ProtocolVersion::parse) + { + Some(protover) => Box::into_raw(Box::new(protover)), + None => null_mut(), + } } } diff --git a/src/drawdance/libmsg/dpmsg/text_writer.h b/src/drawdance/libmsg/dpmsg/text_writer.h index 8493ae5b84..45dfb08b36 100644 --- a/src/drawdance/libmsg/dpmsg/text_writer.h +++ b/src/drawdance/libmsg/dpmsg/text_writer.h @@ -22,11 +22,7 @@ #ifndef DPMSG_TEXT_WRITER_H #define DPMSG_TEXT_WRITER_H #include -#ifdef DP_BUNDLED_PARSON -#include "parson/parson.h" -#else #include -#endif typedef struct DP_Message DP_Message; typedef struct DP_Output DP_Output; diff --git a/src/drawdance/libmsg/test/read_write_roundtrip.c b/src/drawdance/libmsg/test/read_write_roundtrip.c index df352e01e0..47da6298e7 100644 --- a/src/drawdance/libmsg/test/read_write_roundtrip.c +++ b/src/drawdance/libmsg/test/read_write_roundtrip.c @@ -55,7 +55,7 @@ static DP_BinaryReader *open_binary_reader(TEST_PARAMS, const char *path) { DP_Input *bi = DP_file_input_new_from_path(path); FATAL(NOT_NULL_OK(bi, "got binary input for %s", path)); - DP_BinaryReader *br = DP_binary_reader_new(bi); + DP_BinaryReader *br = DP_binary_reader_new(bi, 0); FATAL(NOT_NULL_OK(br, "got binary reader for %s", path)); return br; } @@ -91,7 +91,7 @@ static void write_initial_header(TEST_PARAMS, DP_BinaryWriter *bw, static void write_message_binary(TEST_PARAMS, DP_Message *msg, DP_BinaryWriter *bw) { - OK(DP_binary_writer_write_message(bw, msg), "wrote message to binary"); + OK(DP_binary_writer_write_message(bw, msg) != 0, "wrote message to binary"); } static void write_message_text(TEST_PARAMS, DP_Message *msg, DP_TextWriter *tw) @@ -903,7 +903,8 @@ static void read_binary_messages(TEST_PARAMS) while (true) { DP_Message *msg; - DP_BinaryReaderResult result = DP_binary_reader_read_message(br, &msg); + DP_BinaryReaderResult result = + DP_binary_reader_read_message(br, true, &msg); OK(result == DP_BINARY_READER_SUCCESS || result == DP_BINARY_READER_INPUT_END, "read binary message"); diff --git a/src/drawdance/rust/bindings.rs b/src/drawdance/rust/bindings.rs index 772cee964d..1d35131dd5 100644 --- a/src/drawdance/rust/bindings.rs +++ b/src/drawdance/rust/bindings.rs @@ -763,6 +763,9 @@ pub struct DP_InputMethods { pub seek: ::std::option::Option< unsafe extern "C" fn(internal: *mut ::std::os::raw::c_void, offset: usize) -> bool, >, + pub seek_by: ::std::option::Option< + unsafe extern "C" fn(internal: *mut ::std::os::raw::c_void, size: usize) -> bool, + >, pub dispose: ::std::option::Option, } #[test] @@ -771,7 +774,7 @@ fn bindgen_test_layout_DP_InputMethods() { let ptr = UNINIT.as_ptr(); assert_eq!( ::std::mem::size_of::(), - 48usize, + 56usize, concat!("Size of: ", stringify!(DP_InputMethods)) ); assert_eq!( @@ -830,8 +833,18 @@ fn bindgen_test_layout_DP_InputMethods() { ) ); assert_eq!( - unsafe { ::std::ptr::addr_of!((*ptr).dispose) as usize - ptr as usize }, + unsafe { ::std::ptr::addr_of!((*ptr).seek_by) as usize - ptr as usize }, 40usize, + concat!( + "Offset of field: ", + stringify!(DP_InputMethods), + "::", + stringify!(seek_by) + ) + ); + assert_eq!( + unsafe { ::std::ptr::addr_of!((*ptr).dispose) as usize - ptr as usize }, + 48usize, concat!( "Offset of field: ", stringify!(DP_InputMethods), @@ -876,6 +889,9 @@ extern "C" { extern "C" { pub fn DP_input_seek(input: *mut DP_Input, offset: usize) -> bool; } +extern "C" { + pub fn DP_input_seek_by(input: *mut DP_Input, size: usize) -> bool; +} extern "C" { pub fn DP_file_input_new(fp: *mut FILE, close: bool) -> *mut DP_Input; } @@ -5741,6 +5757,7 @@ extern "C" { context_id: ::std::os::raw::c_uint, buf: *const ::std::os::raw::c_uchar, length: usize, + decode_opaque: bool, ) -> *mut DP_Message; } extern "C" { @@ -8592,6 +8609,14 @@ extern "C" { internal_size: usize, ) -> *mut DP_Message; } +extern "C" { + pub fn DP_message_new_opaque( + type_: DP_MessageType, + context_id: ::std::os::raw::c_uint, + body: *const ::std::os::raw::c_uchar, + length: usize, + ) -> *mut DP_Message; +} extern "C" { pub fn DP_message_incref(msg: *mut DP_Message) -> *mut DP_Message; } @@ -8610,6 +8635,9 @@ extern "C" { extern "C" { pub fn DP_message_type(msg: *mut DP_Message) -> DP_MessageType; } +extern "C" { + pub fn DP_message_opaque(msg: *mut DP_Message) -> bool; +} extern "C" { pub fn DP_message_name(msg: *mut DP_Message) -> *const ::std::os::raw::c_char; } @@ -8631,21 +8659,6 @@ extern "C" { type_: DP_MessageType, ) -> *mut ::std::os::raw::c_void; } -extern "C" { - pub fn DP_message_cast2( - msg: *mut DP_Message, - type1: DP_MessageType, - type2: DP_MessageType, - ) -> *mut ::std::os::raw::c_void; -} -extern "C" { - pub fn DP_message_cast3( - msg: *mut DP_Message, - type1: DP_MessageType, - type2: DP_MessageType, - type3: DP_MessageType, - ) -> *mut ::std::os::raw::c_void; -} extern "C" { pub fn DP_message_length(msg: *mut DP_Message) -> usize; } @@ -8668,12 +8681,14 @@ extern "C" { buf: *const ::std::os::raw::c_uchar, bufsize: usize, body_length: usize, + decode_opaque: bool, ) -> *mut DP_Message; } extern "C" { pub fn DP_message_deserialize( buf: *const ::std::os::raw::c_uchar, bufsize: usize, + decode_opaque: bool, ) -> *mut DP_Message; } extern "C" { diff --git a/src/libclient/CMakeLists.txt b/src/libclient/CMakeLists.txt index c252d81b27..3de441af2f 100644 --- a/src/libclient/CMakeLists.txt +++ b/src/libclient/CMakeLists.txt @@ -71,8 +71,6 @@ target_sources(dpclient PRIVATE drawdance/layerprops.h drawdance/layerpropslist.cpp drawdance/layerpropslist.h - drawdance/message.cpp - drawdance/message.h drawdance/paintengine.cpp drawdance/paintengine.h drawdance/perf.cpp @@ -109,12 +107,10 @@ target_sources(dpclient PRIVATE net/login.h net/loginsessions.cpp net/loginsessions.h - net/messagequeue.cpp - net/messagequeue.h + net/message.cpp + net/message.h net/server.cpp net/server.h - net/servercmd.cpp - net/servercmd.h net/sessionlistingmodel.cpp net/sessionlistingmodel.h net/tcpserver.cpp diff --git a/src/libclient/canvas/canvasmodel.cpp b/src/libclient/canvas/canvasmodel.cpp index 0a4b1af770..f91cf18622 100644 --- a/src/libclient/canvas/canvasmodel.cpp +++ b/src/libclient/canvas/canvasmodel.cpp @@ -1,31 +1,28 @@ // SPDX-License-Identifier: GPL-3.0-or-later - extern "C" { #include #include } - +#include "libclient/canvas/acl.h" #include "libclient/canvas/canvasmodel.h" +#include "libclient/canvas/documentmetadata.h" #include "libclient/canvas/layerlist.h" -#include "libclient/canvas/userlist.h" -#include "libclient/canvas/timelinemodel.h" -#include "libclient/canvas/acl.h" -#include "libclient/canvas/selection.h" #include "libclient/canvas/paintengine.h" -#include "libclient/canvas/documentmetadata.h" +#include "libclient/canvas/selection.h" +#include "libclient/canvas/timelinemodel.h" +#include "libclient/canvas/userlist.h" #include "libclient/settings.h" #include "libclient/utils/identicon.h" #include "libshared/util/qtcompat.h" - #include #include namespace canvas { -CanvasModel::CanvasModel(libclient::settings::Settings &settings, - uint8_t localUserId, int fps, int snapshotMaxCount, - long long snapshotMinDelayMs, bool wantCanvasHistoryDump, - QObject *parent) +CanvasModel::CanvasModel( + libclient::settings::Settings &settings, uint8_t localUserId, int fps, + int snapshotMaxCount, long long snapshotMinDelayMs, + bool wantCanvasHistoryDump, QObject *parent) : QObject(parent) , m_selection(nullptr) , m_localUserId(1) @@ -40,33 +37,59 @@ CanvasModel::CanvasModel(libclient::settings::Settings &settings, m_timeline = new TimelineModel(this); m_metadata = new DocumentMetadata(m_paintengine, this); - connect(m_aclstate, &AclState::userBitsChanged, m_userlist, &UserListModel::updateAclState); - connect(m_paintengine, &PaintEngine::aclsChanged, m_aclstate, &AclState::aclsChanged); - connect(m_paintengine, &PaintEngine::resetLockSet, m_aclstate, &AclState::resetLockSet); - connect(m_paintengine, &PaintEngine::laserTrail, this, &CanvasModel::onLaserTrail); - connect(m_paintengine, &PaintEngine::defaultLayer, m_layerlist, &LayerListModel::setDefaultLayer); - connect(m_paintengine, &PaintEngine::recorderStateChanged, this, &CanvasModel::recorderStateChanged); + connect( + m_aclstate, &AclState::userBitsChanged, m_userlist, + &UserListModel::updateAclState); + connect( + m_paintengine, &PaintEngine::aclsChanged, m_aclstate, + &AclState::aclsChanged); + connect( + m_paintengine, &PaintEngine::resetLockSet, m_aclstate, + &AclState::resetLockSet); + connect( + m_paintengine, &PaintEngine::laserTrail, this, + &CanvasModel::onLaserTrail); + connect( + m_paintengine, &PaintEngine::defaultLayer, m_layerlist, + &LayerListModel::setDefaultLayer); + connect( + m_paintengine, &PaintEngine::recorderStateChanged, this, + &CanvasModel::recorderStateChanged); m_aclstate->setLocalUserId(localUserId); m_layerlist->setAclState(m_aclstate); - m_layerlist->setLayerGetter([this](int id)->QImage { return m_paintengine->getLayerImage(id); }); + m_layerlist->setLayerGetter([this](int id) -> QImage { + return m_paintengine->getLayerImage(id); + }); m_timeline->setAclState(m_aclstate); - connect(m_layerlist, &LayerListModel::autoSelectRequest, this, &CanvasModel::layerAutoselectRequest); - connect(m_paintengine, &PaintEngine::resized, this, &CanvasModel::onCanvasResize, Qt::QueuedConnection); - connect(m_paintengine, &PaintEngine::layersChanged, m_layerlist, &LayerListModel::setLayers); - connect(m_paintengine, &PaintEngine::timelineChanged, m_timeline, &TimelineModel::setTimeline); - connect(m_paintengine, &PaintEngine::frameVisibilityChanged, m_layerlist, &LayerListModel::setLayersVisibleInFrame); + connect( + m_layerlist, &LayerListModel::autoSelectRequest, this, + &CanvasModel::layerAutoselectRequest); + connect( + m_paintengine, &PaintEngine::resized, this, + &CanvasModel::onCanvasResize, Qt::QueuedConnection); + connect( + m_paintengine, &PaintEngine::layersChanged, m_layerlist, + &LayerListModel::setLayers); + connect( + m_paintengine, &PaintEngine::timelineChanged, m_timeline, + &TimelineModel::setTimeline); + connect( + m_paintengine, &PaintEngine::frameVisibilityChanged, m_layerlist, + &LayerListModel::setLayersVisibleInFrame); settings.bindEngineFrameRate(m_paintengine, &PaintEngine::setFps); - settings.bindEngineSnapshotCount(m_paintengine, &PaintEngine::setSnapshotMaxCount); - settings.bindEngineSnapshotInterval(this, [this](int minDelaySec){ + settings.bindEngineSnapshotCount( + m_paintengine, &PaintEngine::setSnapshotMaxCount); + settings.bindEngineSnapshotInterval(this, [this](int minDelaySec) { m_paintengine->setSnapshotMinDelayMs(minDelaySec * 1000LL); }); } -void CanvasModel::loadBlank(int undoDepthLimit, const QSize &size, const QColor &background) +void CanvasModel::loadBlank( + int undoDepthLimit, const QSize &size, const QColor &background) { m_paintengine->enqueueLoadBlank(undoDepthLimit, size, background); } @@ -75,7 +98,8 @@ void CanvasModel::loadCanvasState( int undoDepthLimit, const drawdance::CanvasState &canvasState) { m_paintengine->reset(m_localUserId, canvasState); - drawdance::Message undoDepthMessage = drawdance::Message::makeUndoDepth(0, undoDepthLimit); + net::Message undoDepthMessage = + net::makeUndoDepthMessage(0, undoDepthLimit); m_paintengine->receiveMessages(false, 1, &undoDepthMessage); } @@ -95,7 +119,8 @@ void CanvasModel::previewAnnotation(int id, const QRect &shape) emit previewAnnotationRequested(id, shape); } -void CanvasModel::connectedToServer(uint8_t myUserId, bool join, bool compatibilityMode) +void CanvasModel::connectedToServer( + uint8_t myUserId, bool join, bool compatibilityMode) { if(myUserId == 0) { // Zero is a reserved "null" user ID @@ -126,7 +151,7 @@ void CanvasModel::disconnectedFromServer() emit compatibilityModeChanged(m_compatibilityMode); } -void CanvasModel::handleCommands(int count, const drawdance::Message *msgs) +void CanvasModel::handleCommands(int count, const net::Message *msgs) { handleMetaMessages(count, msgs); if(m_paintengine->receiveMessages(false, count, msgs) != 0) { @@ -134,17 +159,17 @@ void CanvasModel::handleCommands(int count, const drawdance::Message *msgs) } } -void CanvasModel::handleLocalCommands(int count, const drawdance::Message *msgs) +void CanvasModel::handleLocalCommands(int count, const net::Message *msgs) { if(m_paintengine->receiveMessages(true, count, msgs) != 0) { m_layerlist->setAutoselectAny(false); } } -void CanvasModel::handleMetaMessages(int count, const drawdance::Message *msgs) +void CanvasModel::handleMetaMessages(int count, const net::Message *msgs) { - for (int i = 0; i < count; ++i) { - const drawdance::Message &msg = msgs[i]; + for(int i = 0; i < count; ++i) { + const net::Message &msg = msgs[i]; switch(msg.type()) { case DP_MSG_JOIN: handleJoin(msg); @@ -164,7 +189,7 @@ void CanvasModel::handleMetaMessages(int count, const drawdance::Message *msgs) } } -void CanvasModel::handleJoin(const drawdance::Message &msg) +void CanvasModel::handleJoin(const net::Message &msg) { DP_MsgJoin *mj = DP_msg_join_cast(msg.get()); uint8_t user_id = msg.contextId(); @@ -177,14 +202,19 @@ void CanvasModel::handleJoin(const drawdance::Message &msg) const unsigned char *avatarBytes = DP_msg_join_avatar(mj, &avatarSize); QImage avatar; if(avatarSize != 0) { - QByteArray avatarData = QByteArray::fromRawData(reinterpret_cast(avatarBytes), compat::castSize(avatarSize)); + QByteArray avatarData = QByteArray::fromRawData( + reinterpret_cast(avatarBytes), + compat::castSize(avatarSize)); if(!avatar.loadFromData(avatarData)) { - qWarning("Avatar loading failed for user '%s' (#%d)", qPrintable(name), user_id); + qWarning( + "Avatar loading failed for user '%s' (#%d)", qPrintable(name), + user_id); } // Rescale avatar if its the wrong size if(avatar.width() > 32 || avatar.height() > 32) { - avatar = avatar.scaled(32, 32, Qt::KeepAspectRatio, Qt::SmoothTransformation); + avatar = avatar.scaled( + 32, 32, Qt::KeepAspectRatio, Qt::SmoothTransformation); } } if(avatar.isNull()) { @@ -192,7 +222,7 @@ void CanvasModel::handleJoin(const drawdance::Message &msg) } uint8_t flags = DP_msg_join_flags(mj); - const User u { + const User u{ user_id, name, QPixmap::fromImage(avatar), @@ -204,14 +234,13 @@ void CanvasModel::handleJoin(const drawdance::Message &msg) bool(flags & DP_MSG_JOIN_FLAGS_AUTH), false, false, - true - }; + true}; m_userlist->userLogin(u); emit userJoined(user_id, name); } -void CanvasModel::handleLeave(const drawdance::Message &msg) +void CanvasModel::handleLeave(const net::Message &msg) { uint8_t user_id = msg.contextId(); QString name = m_userlist->getUsername(user_id); @@ -219,13 +248,14 @@ void CanvasModel::handleLeave(const drawdance::Message &msg) emit userLeft(user_id, name); } -void CanvasModel::handleChat(const drawdance::Message &msg) +void CanvasModel::handleChat(const net::Message &msg) { DP_MsgChat *mc = DP_msg_chat_cast(msg.get()); size_t messageLength; const char *messageBytes = DP_msg_chat_message(mc, &messageLength); - QString message = QString::fromUtf8(messageBytes, compat::castSize(messageLength)); + QString message = + QString::fromUtf8(messageBytes, compat::castSize(messageLength)); uint8_t oflags = DP_msg_chat_oflags(mc); if(oflags & DP_MSG_CHAT_OFLAGS_PIN) { @@ -244,13 +274,14 @@ void CanvasModel::handleChat(const drawdance::Message &msg) } } -void CanvasModel::handlePrivateChat(const drawdance::Message &msg) +void CanvasModel::handlePrivateChat(const net::Message &msg) { DP_MsgPrivateChat *mpc = DP_msg_private_chat_cast(msg.get()); size_t messageLength; const char *messageBytes = DP_msg_private_chat_message(mpc, &messageLength); - QString message = QString::fromUtf8(messageBytes, compat::castSize(messageLength)); + QString message = + QString::fromUtf8(messageBytes, compat::castSize(messageLength)); uint8_t target = DP_msg_private_chat_target(mpc); uint8_t oflags = DP_msg_private_chat_oflags(mpc); @@ -259,33 +290,34 @@ void CanvasModel::handlePrivateChat(const drawdance::Message &msg) void CanvasModel::onLaserTrail(uint8_t userId, int persistence, uint32_t color) { - emit laserTrail(userId, qMin(15, persistence) * 1000, QColor::fromRgb(color)); + emit laserTrail( + userId, qMin(15, persistence) * 1000, QColor::fromRgb(color)); } -drawdance::MessageList CanvasModel::generateSnapshot( +net::MessageList CanvasModel::generateSnapshot( bool includePinnedMessage, unsigned int aclIncludeFlags) const { - drawdance::MessageList snapshot; + net::MessageList snapshot; m_paintengine->historyCanvasState().toResetImage(snapshot, 0); amendSnapshotMetadata(snapshot, includePinnedMessage, aclIncludeFlags); return snapshot; } void CanvasModel::amendSnapshotMetadata( - drawdance::MessageList &snapshot, bool includePinnedMessage, + net::MessageList &snapshot, bool includePinnedMessage, unsigned int aclIncludeFlags) const { - snapshot.prepend(drawdance::Message::makeUndoDepth( - 0, m_paintengine->undoDepthLimit())); + snapshot.prepend( + net::makeUndoDepthMessage(0, m_paintengine->undoDepthLimit())); if(includePinnedMessage && !m_pinnedMessage.isEmpty()) { - snapshot.prepend(drawdance::Message::makeChat( + snapshot.prepend(net::makeChatMessage( m_localUserId, 0, DP_MSG_CHAT_OFLAGS_PIN, m_pinnedMessage)); } int defaultLayerId = m_layerlist->defaultLayer(); if(defaultLayerId > 0) { - snapshot.append(drawdance::Message::makeDefaultLayer(0, defaultLayerId)); + snapshot.append(net::makeDefaultLayerMessage(0, defaultLayerId)); } m_paintengine->aclState().toResetImage( @@ -376,7 +408,8 @@ QImage CanvasModel::selectionToImage(int layerId) const } else if(layerId == -1) { img = canvasState.toFlatImage(false, true, &rect); } else { - drawdance::LayerContent layerContent = canvasState.searchLayerContent(layerId); + drawdance::LayerContent layerContent = + canvasState.searchLayerContent(layerId); if(layerContent.isNull()) { qWarning("selectionToImage: layer %d not found", layerId); img = QImage(rect.size(), QImage::Format_ARGB32_Premultiplied); @@ -394,12 +427,14 @@ QImage CanvasModel::selectionToImage(int layerId) const QRect maskBounds; const QImage mask = m_selection->shapeMask(Qt::white, &maskBounds); - mp.drawImage(qMin(0, maskBounds.left()), qMin(0, maskBounds.top()), mask); + mp.drawImage( + qMin(0, maskBounds.left()), qMin(0, maskBounds.top()), mask); } return img; } -void CanvasModel::pasteFromImage(const QImage &image, const QPoint &defaultPoint, bool forceDefault) +void CanvasModel::pasteFromImage( + const QImage &image, const QPoint &defaultPoint, bool forceDefault) { QPoint center; if(m_selection && !forceDefault) @@ -408,7 +443,9 @@ void CanvasModel::pasteFromImage(const QImage &image, const QPoint &defaultPoint center = defaultPoint; Selection *paste = new Selection; - paste->setShapeRect(QRect(center.x() - image.width()/2, center.y() - image.height()/2, image.width(), image.height())); + paste->setShapeRect(QRect( + center.x() - image.width() / 2, center.y() - image.height() / 2, + image.width(), image.height())); paste->setPasteImage(image); setSelection(paste); diff --git a/src/libclient/canvas/canvasmodel.h b/src/libclient/canvas/canvasmodel.h index 93a08f9875..1de84b154e 100644 --- a/src/libclient/canvas/canvasmodel.h +++ b/src/libclient/canvas/canvasmodel.h @@ -1,18 +1,15 @@ // SPDX-License-Identifier: GPL-3.0-or-later - #ifndef CANVASMODEL_H #define CANVASMODEL_H - -#include "libclient/drawdance/message.h" #include "libclient/drawdance/paintengine.h" - +#include "libclient/net/message.h" #include #include struct DP_Player; namespace drawdance { - class CanvasState; +class CanvasState; } namespace libclient { @@ -31,8 +28,7 @@ class Selection; class DocumentMetadata; class PaintEngine; -class CanvasModel final : public QObject -{ +class CanvasModel final : public QObject { Q_OBJECT public: CanvasModel( @@ -43,32 +39,41 @@ class CanvasModel final : public QObject PaintEngine *paintEngine() const { return m_paintengine; } //! Load an empty canvas - void loadBlank(int undoDepthLimit, const QSize &size, const QColor &background); + void + loadBlank(int undoDepthLimit, const QSize &size, const QColor &background); - void loadCanvasState(int undoDepthLimit, const drawdance::CanvasState &canvasState); + void loadCanvasState( + int undoDepthLimit, const drawdance::CanvasState &canvasState); //! Prepare to start playback, takes ownership of the given player void loadPlayer(DP_Player *player); QString title() const { return m_title; } - void setTitle(const QString &title) { if(m_title!=title) { m_title = title; emit titleChanged(title); } } + void setTitle(const QString &title) + { + if(m_title != title) { + m_title = title; + emit titleChanged(title); + } + } QString pinnedMessage() const { return m_pinnedMessage; } Selection *selection() const { return m_selection; } void setSelection(Selection *selection); - drawdance::MessageList generateSnapshot( + net::MessageList generateSnapshot( bool includePinnedMessage, unsigned int aclIncludeFlags) const; void amendSnapshotMetadata( - drawdance::MessageList &snapshot, bool includePinnedMessage, + net::MessageList &snapshot, bool includePinnedMessage, unsigned int aclIncludeFlags) const; uint8_t localUserId() const { return m_localUserId; } QImage selectionToImage(int layerId) const; - void pasteFromImage(const QImage &image, const QPoint &defaultPoint, bool forceDefault); + void pasteFromImage( + const QImage &image, const QPoint &defaultPoint, bool forceDefault); void connectedToServer(uint8_t myUserId, bool join, bool compatibilityMode); void disconnectedFromServer(); @@ -96,14 +101,15 @@ class CanvasModel final : public QObject * Request the view layer to preview a change to an annotation * * This is used to preview the change or creation of an annotation. - * If an annotation with the given ID does not exist yet, one will be created. - * The annotation only exists in the view layer and will thus be automatically erased - * or replaced when the actual change goes through. + * If an annotation with the given ID does not exist yet, one will be + * created. The annotation only exists in the view layer and will thus be + * automatically erased or replaced when the actual change goes through. */ void previewAnnotation(int id, const QRect &shape); /** - * Reset the canvas to a blank state, as if the client had just joined a session. + * Reset the canvas to a blank state, as if the client had just joined a + * session. * * This is used to prepare the canvas to receive session reset data. */ @@ -111,13 +117,13 @@ class CanvasModel final : public QObject public slots: //! Handle a meta/command message received from the server - void handleCommands(int count, const drawdance::Message *msgs); + void handleCommands(int count, const net::Message *msgs); //! Handle a local drawing command (will be put in the local fork) - void handleLocalCommands(int count, const drawdance::Message *msgs); + void handleLocalCommands(int count, const net::Message *msgs); void pickLayer(int x, int y); - void pickColor(int x, int y, int layer, int diameter=0); + void pickColor(int x, int y, int layer, int diameter = 0); void inspectCanvas(int x, int y, bool clobber); void inspectCanvas(int contextId); void stopInspectingCanvas(); @@ -138,7 +144,9 @@ public slots: void canvasInspected(int lastEditedBy); void canvasInspectionEnded(); - void chatMessageReceived(int sender, int recipient, uint8_t tflags, uint8_t oflags, const QString &message); + void chatMessageReceived( + int sender, int recipient, uint8_t tflags, uint8_t oflags, + const QString &message); void laserTrail(uint8_t userId, int persistence, const QColor &color); @@ -154,11 +162,11 @@ private slots: void onLaserTrail(uint8_t userId, int persistence, uint32_t color); private: - void handleMetaMessages(int count, const drawdance::Message *msgs); - void handleJoin(const drawdance::Message &msg); - void handleLeave(const drawdance::Message &msg); - void handleChat(const drawdance::Message &msg); - void handlePrivateChat(const drawdance::Message &msg); + void handleMetaMessages(int count, const net::Message *msgs); + void handleJoin(const net::Message &msg); + void handleLeave(const net::Message &msg); + void handleChat(const net::Message &msg); + void handlePrivateChat(const net::Message &msg); AclState *m_aclstate; UserListModel *m_userlist; diff --git a/src/libclient/canvas/paintengine.cpp b/src/libclient/canvas/paintengine.cpp index 8c6082ef68..30124e3a9b 100644 --- a/src/libclient/canvas/paintengine.cpp +++ b/src/libclient/canvas/paintengine.cpp @@ -1,5 +1,4 @@ // SPDX-License-Identifier: GPL-3.0-or-later - extern "C" { #include #include @@ -8,15 +7,14 @@ extern "C" { #include #include } - #include "cmake-config/config.h" #include "libclient/canvas/paintengine.h" #include "libclient/drawdance/image.h" #include "libclient/drawdance/layercontent.h" #include "libclient/drawdance/layerpropslist.h" -#include "libclient/drawdance/message.h" #include "libclient/drawdance/perf.h" #include "libclient/drawdance/viewmode.h" +#include "libclient/net/message.h" #include #include #include @@ -94,7 +92,7 @@ void PaintEngine::reset( uint8_t localUserId, const drawdance::CanvasState &canvasState, DP_Player *player) { - drawdance::MessageList localResetImage = m_paintEngine.reset( + net::MessageList localResetImage = m_paintEngine.reset( m_acls, m_snapshotQueue, localUserId, PaintEngine::onRenderTile, PaintEngine::onRenderUnlock, PaintEngine::onRenderResize, this, PaintEngine::onPlayback, PaintEngine::onDumpPlayback, this, canvasState, @@ -139,40 +137,38 @@ void PaintEngine::updateLayersVisibleInFrame() } int PaintEngine::receiveMessages( - bool local, int count, const drawdance::Message *msgs, bool overrideAcls) + bool local, int count, const net::Message *msgs, bool overrideAcls) { return DP_paint_engine_handle_inc( m_paintEngine.get(), local, overrideAcls, count, - drawdance::Message::asRawMessages(msgs), &PaintEngine::onAclsChanged, + net::Message::asRawMessages(msgs), &PaintEngine::onAclsChanged, &PaintEngine::onLaserTrail, &PaintEngine::onMovePointer, this); } void PaintEngine::enqueueReset() { - drawdance::Message msg = drawdance::Message::makeInternalReset(0); + net::Message msg = net::makeInternalResetMessage(0); receiveMessages(false, 1, &msg); } void PaintEngine::enqueueLoadBlank( int undoDepthLimit, const QSize &size, const QColor &backgroundColor) { - drawdance::Message messages[] = { - drawdance::Message::makeInternalReset(0), - drawdance::Message::makeUndoDepth(0, undoDepthLimit), - drawdance::Message::makeCanvasBackground(0, backgroundColor), - drawdance::Message::makeCanvasResize( - 0, 0, size.width(), size.height(), 0), - drawdance::Message::makeLayerTreeCreate( + net::Message messages[] = { + net::makeInternalResetMessage(0), + net::makeUndoDepthMessage(0, undoDepthLimit), + net::makeCanvasBackgroundMessage(0, backgroundColor), + net::makeCanvasResizeMessage(0, 0, size.width(), size.height(), 0), + net::makeLayerTreeCreateMessage( 0, 0x100, 0, 0, 0, 0, tr("Layer %1").arg(1)), - drawdance::Message::makeInternalSnapshot(0), + net::makeInternalSnapshotMessage(0), }; receiveMessages(false, DP_ARRAY_LENGTH(messages), messages); } void PaintEngine::enqueueCatchupProgress(int progress) { - drawdance::Message msg = - drawdance::Message::makeInternalCatchup(0, progress); + net::Message msg = net::makeInternalCatchupMessage(0, progress); receiveMessages(false, 1, &msg); } @@ -184,7 +180,7 @@ void PaintEngine::resetAcl(uint8_t localUserId) void PaintEngine::cleanup() { - drawdance::Message msg = drawdance::Message::makeInternalCleanup(0); + net::Message msg = net::makeInternalCleanupMessage(0); receiveMessages(false, 1, &msg); } @@ -211,8 +207,7 @@ bool PaintEngine::localBackgroundColor(QColor &outColor) const void PaintEngine::setLocalBackgroundColor(const QColor &color) { - drawdance::Message msg = - drawdance::Message::makeLocalChangeBackgroundColor(color); + net::Message msg = net::makeLocalChangeBackgroundColorMessage(color); if(msg.isNull()) { qWarning("Error setting local background color: %s", DP_error()); } else { @@ -222,8 +217,7 @@ void PaintEngine::setLocalBackgroundColor(const QColor &color) void PaintEngine::clearLocalBackgroundColor() { - drawdance::Message msg = - drawdance::Message::makeLocalChangeBackgroundClear(); + net::Message msg = net::makeLocalChangeBackgroundClearMessage(); receiveMessages(false, 1, &msg); } @@ -291,28 +285,28 @@ void PaintEngine::setLocalDrawingInProgress(bool localDrawingInProgress) void PaintEngine::setLayerVisibility(int layerId, bool hidden) { - drawdance::Message msg = - drawdance::Message::makeLocalChangeLayerVisibility(layerId, hidden); + net::Message msg = + net::makeLocalChangeLayerVisibilityMessage(layerId, hidden); receiveMessages(false, 1, &msg); } void PaintEngine::setTrackVisibility(int trackId, bool hidden) { - drawdance::Message msg = - drawdance::Message::makeLocalChangeTrackVisibility(trackId, hidden); + net::Message msg = + net::makeLocalChangeTrackVisibilityMessage(trackId, hidden); receiveMessages(false, 1, &msg); } void PaintEngine::setTrackOnionSkin(int trackId, bool onionSkin) { - drawdance::Message msg = - drawdance::Message::makeLocalChangeTrackOnionSkin(trackId, onionSkin); + net::Message msg = + net::makeLocalChangeTrackOnionSkinMessage(trackId, onionSkin); receiveMessages(false, 1, &msg); } void PaintEngine::setViewMode(DP_ViewMode vm, bool censor) { - drawdance::Message msg = drawdance::Message::makeLocalChangeViewMode(vm); + net::Message msg = net::makeLocalChangeViewModeMessage(vm); receiveMessages(false, 1, &msg); m_paintEngine.setRevealCensored(!censor); updateLayersVisibleInFrame(); @@ -345,21 +339,20 @@ void PaintEngine::setOnionSkins( DP_upixel15_from_color(skin.second.rgba())); } - drawdance::Message msg = drawdance::Message::makeLocalChangeOnionSkins(oss); + net::Message msg = net::makeLocalChangeOnionSkinsMessage(oss); receiveMessages(false, 1, &msg); DP_onion_skins_free(oss); } void PaintEngine::setViewLayer(int id) { - drawdance::Message msg = drawdance::Message::makeLocalChangeActiveLayer(id); + net::Message msg = net::makeLocalChangeActiveLayerMessage(id); receiveMessages(false, 1, &msg); } void PaintEngine::setViewFrame(int frame) { - drawdance::Message msg = - drawdance::Message::makeLocalChangeActiveFrame(frame); + net::Message msg = net::makeLocalChangeActiveFrameMessage(frame); receiveMessages(false, 1, &msg); updateLayersVisibleInFrame(); } @@ -431,7 +424,7 @@ drawdance::RecordStartResult PaintEngine::startRecording(const QString &path) } drawdance::RecordStartResult PaintEngine::exportTemplate( - const QString &path, const drawdance::MessageList &snapshot) + const QString &path, const net::MessageList &snapshot) { return m_paintEngine.exportTemplate( path, snapshot, "drawpile", cmake_config::version(), "template"); @@ -449,7 +442,7 @@ bool PaintEngine::isRecording() const DP_PlayerResult PaintEngine::stepPlayback(long long steps) { - drawdance::MessageList msgs; + net::MessageList msgs; DP_PlayerResult result = m_paintEngine.stepPlayback(steps, msgs); receiveMessages(false, msgs.count(), msgs.constData(), true); return result; @@ -457,7 +450,7 @@ DP_PlayerResult PaintEngine::stepPlayback(long long steps) DP_PlayerResult PaintEngine::skipPlaybackBy(long long steps, bool bySnapshots) { - drawdance::MessageList msgs; + net::MessageList msgs; DP_PlayerResult result = m_paintEngine.skipPlaybackBy(steps, bySnapshots, msgs); receiveMessages(false, msgs.count(), msgs.constData(), true); @@ -466,7 +459,7 @@ DP_PlayerResult PaintEngine::skipPlaybackBy(long long steps, bool bySnapshots) DP_PlayerResult PaintEngine::jumpPlaybackTo(long long position) { - drawdance::MessageList msgs; + net::MessageList msgs; DP_PlayerResult result = m_paintEngine.jumpPlaybackTo(position, msgs); receiveMessages(false, msgs.count(), msgs.constData(), true); return result; @@ -479,7 +472,7 @@ DP_PlayerResult PaintEngine::beginPlayback() DP_PlayerResult PaintEngine::playPlayback(long long msecs) { - drawdance::MessageList msgs; + net::MessageList msgs; DP_PlayerResult result = m_paintEngine.playPlayback(msecs, msgs); receiveMessages(false, msgs.count(), msgs.constData(), true); return result; @@ -513,7 +506,7 @@ QImage PaintEngine::playbackIndexThumbnailAt(size_t index) DP_PlayerResult PaintEngine::stepDumpPlayback() { - drawdance::MessageList msgs; + net::MessageList msgs; DP_PlayerResult result = m_paintEngine.stepDumpPlayback(msgs); receiveMessages(false, msgs.count(), msgs.constData(), true); return result; @@ -521,7 +514,7 @@ DP_PlayerResult PaintEngine::stepDumpPlayback() DP_PlayerResult PaintEngine::jumpDumpPlaybackToPreviousReset() { - drawdance::MessageList msgs; + net::MessageList msgs; DP_PlayerResult result = m_paintEngine.jumpDumpPlaybackToPreviousReset(msgs); receiveMessages(false, msgs.count(), msgs.constData(), true); @@ -530,7 +523,7 @@ DP_PlayerResult PaintEngine::jumpDumpPlaybackToPreviousReset() DP_PlayerResult PaintEngine::jumpDumpPlaybackToNextReset() { - drawdance::MessageList msgs; + net::MessageList msgs; DP_PlayerResult result = m_paintEngine.jumpDumpPlaybackToNextReset(msgs); receiveMessages(false, msgs.count(), msgs.constData(), true); return result; @@ -538,7 +531,7 @@ DP_PlayerResult PaintEngine::jumpDumpPlaybackToNextReset() DP_PlayerResult PaintEngine::jumpDumpPlayback(long long position) { - drawdance::MessageList msgs; + net::MessageList msgs; DP_PlayerResult result = m_paintEngine.jumpDumpPlayback(position, msgs); receiveMessages(false, msgs.count(), msgs.constData(), true); return result; @@ -546,7 +539,7 @@ DP_PlayerResult PaintEngine::jumpDumpPlayback(long long position) bool PaintEngine::flushPlayback() { - drawdance::MessageList msgs; + net::MessageList msgs; bool ok = m_paintEngine.flushPlayback(msgs); receiveMessages(false, msgs.count(), msgs.constData()); return ok; @@ -581,7 +574,7 @@ void PaintEngine::clearTransformPreview() m_paintEngine.clearTransformPreview(); } -void PaintEngine::previewDabs(int layerId, const drawdance::MessageList &msgs) +void PaintEngine::previewDabs(int layerId, const net::MessageList &msgs) { m_paintEngine.previewDabs(layerId, msgs.count(), msgs.constData()); } diff --git a/src/libclient/canvas/paintengine.h b/src/libclient/canvas/paintengine.h index d43a6720c7..e0b138fade 100644 --- a/src/libclient/canvas/paintengine.h +++ b/src/libclient/canvas/paintengine.h @@ -1,22 +1,18 @@ // SPDX-License-Identifier: GPL-3.0-or-later - #ifndef PAINTENGINE_H #define PAINTENGINE_H - extern "C" { #include } - -#include -#include -#include -#include - #include "libclient/drawdance/aclstate.h" #include "libclient/drawdance/canvashistory.h" #include "libclient/drawdance/canvasstate.h" #include "libclient/drawdance/paintengine.h" #include "libclient/drawdance/snapshotqueue.h" +#include +#include +#include +#include struct DP_Mutex; struct DP_Semaphore; @@ -82,7 +78,7 @@ class PaintEngine final : public QObject { //! Receive and handle messages, returns how many messages were actually //! pushed to the paint engine. int receiveMessages( - bool local, int count, const drawdance::Message *msgs, + bool local, int count, const net::Message *msgs, bool overrideAcls = false); void enqueueReset(); @@ -188,7 +184,7 @@ class PaintEngine final : public QObject { drawdance::RecordStartResult startRecording(const QString &path); drawdance::RecordStartResult - exportTemplate(const QString &path, const drawdance::MessageList &snapshot); + exportTemplate(const QString &path, const net::MessageList &snapshot); bool stopRecording(); bool isRecording() const; @@ -216,7 +212,7 @@ class PaintEngine final : public QObject { int layerId, int x, int y, const QImage &img, const QPolygon &dstPolygon, int interpolation); void clearTransformPreview(); - void previewDabs(int layerId, const drawdance::MessageList &msgs); + void previewDabs(int layerId, const net::MessageList &msgs); void clearDabsPreview(); signals: diff --git a/src/libclient/canvas/selection.cpp b/src/libclient/canvas/selection.cpp index 29889316a9..681dd30ff1 100644 --- a/src/libclient/canvas/selection.cpp +++ b/src/libclient/canvas/selection.cpp @@ -1,7 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later - #include "libclient/canvas/selection.h" -#include "libclient/drawdance/message.h" +#include "libclient/net/message.h" #include "libclient/tools/selection.h" // for selection utilities #include #include @@ -600,16 +599,15 @@ void Selection::setMoveImage( } static void appendPutImage( - drawdance::MessageList &buffer, uint8_t contextId, uint16_t layer, int x, - int y, const QImage &image, DP_BlendMode mode) + net::MessageList &buffer, uint8_t contextId, uint16_t layer, int x, int y, + const QImage &image, DP_BlendMode mode) { - drawdance::Message::makePutImages( - buffer, contextId, layer, mode, x, y, image); + net::makePutImageMessages(buffer, contextId, layer, mode, x, y, image); } bool Selection::pasteOrMoveToCanvas( - drawdance::MessageList &buffer, uint8_t contextId, int layer, - int interpolation, bool compatibilityMode) const + net::MessageList &buffer, uint8_t contextId, int layer, int interpolation, + bool compatibilityMode) const { if(m_pasteImage.isNull()) { qWarning("Selection::pasteToCanvas: nothing to paste"); @@ -640,7 +638,7 @@ bool Selection::pasteOrMoveToCanvas( if(compatibilityMode) { QPolygon s = m_shape.toPolygon(); // TODO: moving between different layers via putimage? - drawdance::Message msg = drawdance::Message::makeMoveRegion( + net::Message msg = net::makeMoveRegionMessage( contextId, layer, moveBounds.x(), moveBounds.y(), moveBounds.width(), moveBounds.height(), s[0].x(), s[0].y(), s[1].x(), s[1].y(), s[2].x(), s[2].y(), s[3].x(), s[3].y(), @@ -649,11 +647,11 @@ bool Selection::pasteOrMoveToCanvas( qWarning("Transform: mask too large"); return false; } else { - buffer.append(drawdance::Message::makeUndoPoint(contextId)); + buffer.append(net::makeUndoPointMessage(contextId)); buffer.append(msg); } } else if(isOnlyTranslated()) { - drawdance::Message msg = drawdance::Message::makeMoveRect( + net::Message msg = net::makeMoveRectMessage( contextId, layer, m_sourceLayerId, moveBounds.x(), moveBounds.y(), m_shape.at(0).x(), m_shape.at(0).y(), moveBounds.width(), moveBounds.height(), mask); @@ -661,12 +659,12 @@ bool Selection::pasteOrMoveToCanvas( qWarning("Translate: mask too large"); return false; } else { - buffer.append(drawdance::Message::makeUndoPoint(contextId)); + buffer.append(net::makeUndoPointMessage(contextId)); buffer.append(msg); } } else { QPolygon s = m_shape.toPolygon(); - drawdance::Message msg = drawdance::Message::makeTransformRegion( + net::Message msg = net::makeTransformRegionMessage( contextId, layer, m_sourceLayerId, moveBounds.x(), moveBounds.y(), moveBounds.width(), moveBounds.height(), s[0].x(), s[0].y(), s[1].x(), s[1].y(), s[2].x(), s[2].y(), @@ -675,7 +673,7 @@ bool Selection::pasteOrMoveToCanvas( qWarning("Transform: mask too large"); return false; } else { - buffer.append(drawdance::Message::makeUndoPoint(contextId)); + buffer.append(net::makeUndoPointMessage(contextId)); buffer.append(msg); } } @@ -685,7 +683,7 @@ bool Selection::pasteOrMoveToCanvas( QPoint offset; QImage image = tools::SelectionTool::transformSelectionImage( m_pasteImage, m_shape.toPolygon(), &offset); - buffer.append(drawdance::Message::makeUndoPoint(contextId)); + buffer.append(net::makeUndoPointMessage(contextId)); appendPutImage( buffer, contextId, layer, offset.x(), offset.y(), image, DP_BLEND_MODE_NORMAL); @@ -707,7 +705,7 @@ QPolygon Selection::destinationQuad() const } bool Selection::fillCanvas( - drawdance::MessageList &buffer, uint8_t contextId, const QColor &color, + net::MessageList &buffer, uint8_t contextId, const QColor &color, DP_BlendMode mode, int layer, bool source) const { QRect area; @@ -721,10 +719,10 @@ bool Selection::fillCanvas( } if(!area.isEmpty() || !mask.isNull()) { - buffer.append(drawdance::Message::makeUndoPoint(contextId)); + buffer.append(net::makeUndoPointMessage(contextId)); if(mask.isNull()) { - buffer.append(drawdance::Message::makeFillRect( + buffer.append(net::makeFillRectMessage( contextId, layer, mode, area.x(), area.y(), area.width(), area.height(), color)); } else { diff --git a/src/libclient/canvas/selection.h b/src/libclient/canvas/selection.h index 1fddb209b3..25b5d93a47 100644 --- a/src/libclient/canvas/selection.h +++ b/src/libclient/canvas/selection.h @@ -1,14 +1,10 @@ // SPDX-License-Identifier: GPL-3.0-or-later - #ifndef SELECTION_H #define SELECTION_H - extern "C" { #include } - -#include "libclient/drawdance/message.h" - +#include "libclient/net/message.h" #include #include #include @@ -182,7 +178,7 @@ class Selection final : public QObject { * @return set of commands */ bool pasteOrMoveToCanvas( - drawdance::MessageList &buffer, uint8_t contextId, int layer, + net::MessageList &buffer, uint8_t contextId, int layer, int interpolation, bool compatibilityMode) const; /** @@ -200,7 +196,7 @@ class Selection final : public QObject { * @return set of commands */ bool fillCanvas( - drawdance::MessageList &buffer, uint8_t contextId, const QColor &color, + net::MessageList &buffer, uint8_t contextId, const QColor &color, DP_BlendMode mode, int layer, bool source = false) const; /** diff --git a/src/libclient/canvas/userlist.cpp b/src/libclient/canvas/userlist.cpp index 2988a1a9d8..92a4d14073 100644 --- a/src/libclient/canvas/userlist.cpp +++ b/src/libclient/canvas/userlist.cpp @@ -1,17 +1,11 @@ // SPDX-License-Identifier: GPL-3.0-or-later - -extern "C" { -#include -} - #include "libclient/canvas/userlist.h" #include "libclient/canvas/acl.h" - #include #include #include -#include #include +#include namespace canvas { @@ -23,7 +17,7 @@ UserListModel::UserListModel(QObject *parent) } -QVariant UserListModel::data(const QModelIndex& index, int role) const +QVariant UserListModel::data(const QModelIndex &index, int role) const { if(!index.isValid() || index.row() < 0 || index.row() >= m_users.size()) return QVariant(); @@ -31,27 +25,39 @@ QVariant UserListModel::data(const QModelIndex& index, int role) const const User &u = m_users.at(index.row()); if(role == Qt::ForegroundRole && !u.isOnline) { - return QPalette().color(QPalette::ColorGroup::Disabled, QPalette::ColorRole::WindowText); + return QPalette().color( + QPalette::ColorGroup::Disabled, QPalette::ColorRole::WindowText); } if(index.column() == 0) { switch(role) { - case IdRole: return u.id; - case Qt::DisplayRole: - case NameRole: return u.name; - case Qt::DecorationRole: - case AvatarRole: return u.avatar; - case IsOpRole: return u.isOperator; - case IsTrustedRole: return u.isTrusted; - case IsModRole: return u.isMod; - case IsAuthRole: return u.isAuth; - case IsBotRole: return u.isBot; - case IsLockedRole: return u.isLocked; - case IsMutedRole: return u.isMuted; - case IsOnlineRole: return u.isOnline; + case IdRole: + return u.id; + case Qt::DisplayRole: + case NameRole: + return u.name; + case Qt::DecorationRole: + case AvatarRole: + return u.avatar; + case IsOpRole: + return u.isOperator; + case IsTrustedRole: + return u.isTrusted; + case IsModRole: + return u.isMod; + case IsAuthRole: + return u.isAuth; + case IsBotRole: + return u.isBot; + case IsLockedRole: + return u.isLocked; + case IsMutedRole: + return u.isMuted; + case IsOnlineRole: + return u.isOnline; } - } else if(role==Qt::DisplayRole) { + } else if(role == Qt::DisplayRole) { switch(index.column()) { case 1: if(u.isMod) @@ -65,48 +71,58 @@ QVariant UserListModel::data(const QModelIndex& index, int role) const else return QVariant(); - case 2: return u.isOnline ? tr("Online") : tr("Offline"); + case 2: + return u.isOnline ? tr("Online") : tr("Offline"); } - } else if(role==Qt::DecorationRole) { + } else if(role == Qt::DecorationRole) { switch(index.column()) { - case 3: return u.isLocked ? QIcon::fromTheme("object-locked") : QVariant(); - case 4: return u.isMuted ? QIcon::fromTheme("irc-unvoice") : QVariant(); + case 3: + return u.isLocked ? QIcon::fromTheme("object-locked") : QVariant(); + case 4: + return u.isMuted ? QIcon::fromTheme("irc-unvoice") : QVariant(); } } return QVariant(); } -int UserListModel::columnCount(const QModelIndex&) const +int UserListModel::columnCount(const QModelIndex &) const { return 5; } -int UserListModel::rowCount(const QModelIndex& parent) const +int UserListModel::rowCount(const QModelIndex &parent) const { if(parent.isValid()) return 0; return m_users.count(); } -QVariant UserListModel::headerData(int section, Qt::Orientation orientation, int role) const +QVariant UserListModel::headerData( + int section, Qt::Orientation orientation, int role) const { if(orientation == Qt::Horizontal) { if(role == Qt::DisplayRole) { switch(section) { - case 0: return tr("User"); - case 1: return tr("Type"); - case 2: return tr("Status"); + case 0: + return tr("User"); + case 1: + return tr("Type"); + case 2: + return tr("Status"); } } else if(role == Qt::DecorationRole) { switch(section) { - case 3: return QIcon::fromTheme("object-locked"); - case 4: return QIcon::fromTheme("irc-unvoice"); + case 3: + return QIcon::fromTheme("object-locked"); + case 4: + return QIcon::fromTheme("irc-unvoice"); } } - } else if(section >=0 && section < m_users.size() && role == Qt::DisplayRole) { + } else if( + section >= 0 && section < m_users.size() && role == Qt::DisplayRole) { return m_users.at(section).id; } @@ -116,7 +132,7 @@ QVariant UserListModel::headerData(int section, Qt::Orientation orientation, int void UserListModel::userLogin(const User &user) { // Check if this is a returning user - for(int i=0;i UserListModel::operatorList() const { QVector ops; - for(int i=0;i UserListModel::operatorList() const QVector UserListModel::lockList() const { QVector locks; - for(int i=0;i UserListModel::lockList() const QVector UserListModel::trustedList() const { QVector ids; - for(int i=0;i0 && userId<255); + Q_ASSERT(userId > 0 && userId < 255); QVector ids = lockList(); if(lock) { @@ -300,12 +315,13 @@ drawdance::Message UserListModel::getLockUserCommand(int localId, int userId, bo ids.removeAll(userId); } - return drawdance::Message::makeUserAcl(localId, ids); + return net::makeUserAclMessage(localId, ids); } -drawdance::Message UserListModel::getOpUserCommand(int localId, int userId, bool op) const +net::Message +UserListModel::getOpUserCommand(int localId, int userId, bool op) const { - Q_ASSERT(userId>0 && userId<255); + Q_ASSERT(userId > 0 && userId < 255); QVector ops = operatorList(); if(op) { @@ -315,12 +331,13 @@ drawdance::Message UserListModel::getOpUserCommand(int localId, int userId, bool ops.removeOne(userId); } - return drawdance::Message::makeSessionOwner(localId, ops); + return net::makeSessionOwnerMessage(localId, ops); } -drawdance::Message UserListModel::getTrustUserCommand(int localId, int userId, bool trust) const +net::Message +UserListModel::getTrustUserCommand(int localId, int userId, bool trust) const { - Q_ASSERT(userId>0 && userId<255); + Q_ASSERT(userId > 0 && userId < 255); QVector trusted = trustedList(); if(trust) { @@ -330,13 +347,15 @@ drawdance::Message UserListModel::getTrustUserCommand(int localId, int userId, b trusted.removeOne(userId); } - return drawdance::Message::makeTrustedUsers(localId, trusted); + return net::makeTrustedUsersMessage(localId, trusted); } -bool OnlineUserListModel::filterAcceptsRow(int source_row, const QModelIndex &parent) const +bool OnlineUserListModel::filterAcceptsRow( + int source_row, const QModelIndex &parent) const { const auto i = sourceModel()->index(source_row, 0); - return i.data(UserListModel::IsOnlineRole).toBool() && QSortFilterProxyModel::filterAcceptsRow(source_row, parent); + return i.data(UserListModel::IsOnlineRole).toBool() && + QSortFilterProxyModel::filterAcceptsRow(source_row, parent); } } diff --git a/src/libclient/canvas/userlist.h b/src/libclient/canvas/userlist.h index 5c971cfb18..8e7badbc91 100644 --- a/src/libclient/canvas/userlist.h +++ b/src/libclient/canvas/userlist.h @@ -1,14 +1,11 @@ // SPDX-License-Identifier: GPL-3.0-or-later - #ifndef DP_NET_USERLISTMODEL_H #define DP_NET_USERLISTMODEL_H - -#include "libclient/drawdance/message.h" - +#include "libclient/net/message.h" #include -#include #include #include +#include #include class QJsonArray; @@ -57,12 +54,15 @@ class UserListModel final : public QAbstractTableModel { IsOnlineRole }; - UserListModel(QObject *parent=nullptr); + UserListModel(QObject *parent = nullptr); - QVariant data(const QModelIndex& index, int role=Qt::DisplayRole) const override; - int rowCount(const QModelIndex& parent=QModelIndex()) const override; - int columnCount(const QModelIndex& parent=QModelIndex()) const override; - QVariant headerData(int section, Qt::Orientation orientation, int role=Qt::DisplayRole) const override; + QVariant + data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant headerData( + int section, Qt::Orientation orientation, + int role = Qt::DisplayRole) const override; //! A new user logs in void userLogin(const User &user); @@ -118,7 +118,7 @@ class UserListModel final : public QAbstractTableModel { * @param lock * @return */ - drawdance::Message getLockUserCommand(int localId, int userId, bool lock) const; + net::Message getLockUserCommand(int localId, int userId, bool lock) const; /** * @brief Get the command for granting or revoking operator privileges @@ -127,7 +127,7 @@ class UserListModel final : public QAbstractTableModel { * @param op * @return */ - drawdance::Message getOpUserCommand(int localId, int userId, bool op) const; + net::Message getOpUserCommand(int localId, int userId, bool op) const; /** * @brief Get the command for granting or revoking trusted status @@ -136,13 +136,13 @@ class UserListModel final : public QAbstractTableModel { * @param trusted * @return */ - drawdance::Message getTrustUserCommand(int localId, int userId, bool op) const; + net::Message getTrustUserCommand(int localId, int userId, bool op) const; /** - * @brief Check if the given user is an operator - * @param userId - * @return - */ + * @brief Check if the given user is an operator + * @param userId + * @return + */ bool isOperator(int userId) const; public slots: @@ -157,14 +157,14 @@ public slots: /** * A filtered user list model that only includes online users */ -class OnlineUserListModel final : public QSortFilterProxyModel -{ +class OnlineUserListModel final : public QSortFilterProxyModel { Q_OBJECT public: using QSortFilterProxyModel::QSortFilterProxyModel; protected: - bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override; + bool filterAcceptsRow( + int source_row, const QModelIndex &source_parent) const override; }; } @@ -172,4 +172,3 @@ class OnlineUserListModel final : public QSortFilterProxyModel Q_DECLARE_METATYPE(canvas::User) #endif - diff --git a/src/libclient/document.cpp b/src/libclient/document.cpp index 00d06f1f79..c629ab333b 100644 --- a/src/libclient/document.cpp +++ b/src/libclient/document.cpp @@ -1,66 +1,66 @@ // SPDX-License-Identifier: GPL-3.0-or-later - #include "libclient/document.h" - -#include "libclient/net/client.h" -#include "libclient/net/servercmd.h" #include "libclient/canvas/canvasmodel.h" +#include "libclient/canvas/layerlist.h" #include "libclient/canvas/paintengine.h" #include "libclient/canvas/selection.h" -#include "libclient/canvas/layerlist.h" #include "libclient/canvas/userlist.h" #include "libclient/export/canvassaverrunnable.h" +#include "libclient/net/client.h" #include "libclient/settings.h" #include "libclient/tools/selection.h" #include "libclient/tools/toolcontroller.h" #include "libclient/utils/images.h" +#include "libshared/net/servercmd.h" #include "libshared/util/functionrunnable.h" #include "libshared/util/qtcompat.h" - -#include -#include -#include #include -#include +#include +#include #include -#include #include #include -#include +#include +#include +#include #include +#include Document::Document(libclient::settings::Settings &settings, QObject *parent) - : QObject(parent), - m_resetstate(), - m_messageBuffer(), - m_canvas(nullptr), - m_settings(settings), - m_dirty(false), - m_autosave(false), - m_canAutosave(false), - m_saveInProgress(false), - m_wantCanvasHistoryDump(false), - m_sessionPersistent(false), - m_sessionClosed(false), - m_sessionAuthOnly(false), - m_sessionPreserveChat(false), - m_sessionPasswordProtected(false), - m_sessionOpword(false), - m_sessionNsfm(false), - m_sessionForceNsfm(false), - m_sessionDeputies(false), - m_sessionMaxUserCount(0), - m_sessionHistoryMaxSize(0), - m_sessionResetThreshold(0), - m_baseResetThreshold(0) + : QObject(parent) + , m_resetstate() + , m_messageBuffer() + , m_canvas(nullptr) + , m_settings(settings) + , m_dirty(false) + , m_autosave(false) + , m_canAutosave(false) + , m_saveInProgress(false) + , m_wantCanvasHistoryDump(false) + , m_sessionPersistent(false) + , m_sessionClosed(false) + , m_sessionAuthOnly(false) + , m_sessionPreserveChat(false) + , m_sessionPasswordProtected(false) + , m_sessionOpword(false) + , m_sessionNsfm(false) + , m_sessionForceNsfm(false) + , m_sessionDeputies(false) + , m_sessionMaxUserCount(0) + , m_sessionHistoryMaxSize(0) + , m_sessionResetThreshold(0) + , m_baseResetThreshold(0) { // Initialize m_client = new net::Client(this); - m_settings.bindMessageQueueDrainRate(m_client, &net::Client::setSmoothDrainRate); + m_settings.bindMessageQueueDrainRate( + m_client, &net::Client::setSmoothDrainRate); m_toolctrl = new tools::ToolController(m_client, this); - m_settings.bindSmoothing(m_toolctrl, &tools::ToolController::setGlobalSmoothing); + m_settings.bindSmoothing( + m_toolctrl, &tools::ToolController::setGlobalSmoothing); m_banlist = new net::BanlistModel(this); - m_announcementlist = new net::AnnouncementListModel(settings.listServers(), this); + m_announcementlist = + new net::AnnouncementListModel(settings.listServers(), this); m_serverLog = new QStringListModel(this); m_autosaveTimer = new QTimer(this); @@ -68,19 +68,36 @@ Document::Document(libclient::settings::Settings &settings, QObject *parent) connect(m_autosaveTimer, &QTimer::timeout, this, &Document::autosaveNow); // Make connections - connect(m_client, &net::Client::serverConnected, this, &Document::serverConnected); - connect(m_client, &net::Client::serverLoggedIn, this, &Document::onServerLogin); - connect(m_client, &net::Client::serverDisconnected, this, &Document::onServerDisconnect); - connect(m_client, &net::Client::serverDisconnected, this, &Document::serverDisconnected); - - connect(m_client, &net::Client::needSnapshot, this, &Document::snapshotNeeded); - connect(m_client, &net::Client::sessionConfChange, this, &Document::onSessionConfChanged); - connect(m_client, &net::Client::autoresetRequested, this, &Document::onAutoresetRequested); - connect(m_client, &net::Client::serverLog, this, &Document::addServerLogEntry); - - connect(m_client, &net::Client::sessionResetted, this, &Document::onSessionResetted); - - connect(this, &Document::justInTimeSnapshotGenerated, this, &Document::sendResetSnapshot, Qt::QueuedConnection); + connect( + m_client, &net::Client::serverConnected, this, + &Document::serverConnected); + connect( + m_client, &net::Client::serverLoggedIn, this, &Document::onServerLogin); + connect( + m_client, &net::Client::serverDisconnected, this, + &Document::onServerDisconnect); + connect( + m_client, &net::Client::serverDisconnected, this, + &Document::serverDisconnected); + + connect( + m_client, &net::Client::needSnapshot, this, &Document::snapshotNeeded); + connect( + m_client, &net::Client::sessionConfChange, this, + &Document::onSessionConfChanged); + connect( + m_client, &net::Client::autoresetRequested, this, + &Document::onAutoresetRequested); + connect( + m_client, &net::Client::serverLog, this, &Document::addServerLogEntry); + + connect( + m_client, &net::Client::sessionResetted, this, + &Document::onSessionResetted); + + connect( + this, &Document::justInTimeSnapshotGenerated, this, + &Document::sendResetSnapshot, Qt::QueuedConnection); } void Document::initCanvas() @@ -93,21 +110,37 @@ void Document::initCanvas() m_settings.engineFrameRate(), m_settings.engineSnapshotCount(), m_settings.engineSnapshotInterval() * 1000LL, - m_wantCanvasHistoryDump, this - }; + m_wantCanvasHistoryDump, + this}; m_toolctrl->setModel(m_canvas); - connect(m_client, &net::Client::messagesReceived, m_canvas, &canvas::CanvasModel::handleCommands); - connect(m_client, &net::Client::drawingCommandsLocal, m_canvas, &canvas::CanvasModel::handleLocalCommands); - connect(m_canvas, &canvas::CanvasModel::canvasModified, this, &Document::markDirty); - connect(m_canvas->layerlist(), &canvas::LayerListModel::moveRequested, this, &Document::onMoveLayerRequested); - - connect(m_canvas, &canvas::CanvasModel::titleChanged, this, &Document::sessionTitleChanged); - connect(m_canvas, &canvas::CanvasModel::recorderStateChanged, this, &Document::recorderStateChanged); - - connect(m_client, &net::Client::catchupProgress, m_canvas->paintEngine(), &canvas::PaintEngine::enqueueCatchupProgress); - connect(m_canvas->paintEngine(), &canvas::PaintEngine::caughtUpTo, this, &Document::catchupProgress, Qt::QueuedConnection); + connect( + m_client, &net::Client::messagesReceived, m_canvas, + &canvas::CanvasModel::handleCommands); + connect( + m_client, &net::Client::drawingCommandsLocal, m_canvas, + &canvas::CanvasModel::handleLocalCommands); + connect( + m_canvas, &canvas::CanvasModel::canvasModified, this, + &Document::markDirty); + connect( + m_canvas->layerlist(), &canvas::LayerListModel::moveRequested, this, + &Document::onMoveLayerRequested); + + connect( + m_canvas, &canvas::CanvasModel::titleChanged, this, + &Document::sessionTitleChanged); + connect( + m_canvas, &canvas::CanvasModel::recorderStateChanged, this, + &Document::recorderStateChanged); + + connect( + m_client, &net::Client::catchupProgress, m_canvas->paintEngine(), + &canvas::PaintEngine::enqueueCatchupProgress); + connect( + m_canvas->paintEngine(), &canvas::PaintEngine::caughtUpTo, this, + &Document::catchupProgress, Qt::QueuedConnection); emit canvasChanged(m_canvas); m_canvas->paintEngine()->resetAcl(m_client->myId()); @@ -130,7 +163,8 @@ void Document::onSessionResetted() emit sessionResetState(m_canvas->paintEngine()->viewCanvasState()); - // Clear out the canvas in preparation for the new data that is about to follow + // Clear out the canvas in preparation for the new data that is about to + // follow m_canvas->resetCanvas(); m_resetstate.clear(); } @@ -149,7 +183,8 @@ bool Document::loadBlank(const QSize &size, const QColor &background) DP_LoadResult Document::loadFile(const QString &path) { DP_LoadResult result; - drawdance::CanvasState canvasState = drawdance::CanvasState::load(path, &result); + drawdance::CanvasState canvasState = + drawdance::CanvasState::load(path, &result); if(canvasState.isNull()) { Q_ASSERT(result != DP_LOAD_RESULT_SUCCESS); return result; @@ -166,20 +201,21 @@ DP_LoadResult Document::loadFile(const QString &path) static bool isSessionTemplate(DP_Player *player) { JSON_Value *header = DP_player_header(player); - return header && DP_str_equal( - json_object_get_string(json_value_get_object(header), "type"), - "template"); + return header && + DP_str_equal( + json_object_get_string(json_value_get_object(header), "type"), + "template"); } DP_LoadResult Document::loadRecording( const QString &path, bool debugDump, bool *outIsTemplate) { DP_LoadResult result; - DP_Player *player = debugDump - ? DP_load_debug_dump(path.toUtf8().constData(), &result) - : DP_load_recording(path.toUtf8().constData(), &result); + DP_Player *player = + debugDump ? DP_load_debug_dump(path.toUtf8().constData(), &result) + : DP_load_recording(path.toUtf8().constData(), &result); bool isTemplate; - switch (result) { + switch(result) { case DP_LOAD_RESULT_SUCCESS: setAutosave(false); initCanvas(); @@ -199,13 +235,14 @@ DP_LoadResult Document::loadRecording( isTemplate = false; break; } - if(outIsTemplate){ + if(outIsTemplate) { *outIsTemplate = isTemplate; } return result; } -void Document::onServerLogin(bool join, bool compatibilityMode, const QString &joinPassword) +void Document::onServerLogin( + bool join, bool compatibilityMode, const QString &joinPassword) { if(join) initCanvas(); @@ -290,11 +327,9 @@ void Document::onSessionConfChanged(const QJsonObject &config) QString jc; for(auto v : config["announcements"].toArray()) { const QJsonObject o = v.toObject(); - const net::Announcement a { - o["url"].toString(), - o["roomcode"].toString(), - o["private"].toBool() - }; + const net::Announcement a{ + o["url"].toString(), o["roomcode"].toString(), + o["private"].toBool()}; m_announcementlist->addAnnouncement(a); if(!a.roomcode.isEmpty()) jc = a.roomcode; @@ -310,7 +345,8 @@ void Document::onAutoresetRequested(int maxSize, bool query) qInfo("Server requested autoreset (query=%d)", query); if(!m_client->isFullyCaughtUp()) { - qInfo("Ignoring autoreset request because we're not fully caught up yet."); + qInfo("Ignoring autoreset request because we're not fully caught up " + "yet."); return; } @@ -319,7 +355,8 @@ void Document::onAutoresetRequested(int maxSize, bool query) if(m_settings.serverAutoReset()) { if(query) { // This is just a query: send back an affirmative response - m_client->sendMessage(net::ServerCommand::make("ready-to-autoreset")); + m_client->sendMessage( + net::ServerCommand::make("ready-to-autoreset")); } else { // Autoreset on request @@ -328,8 +365,9 @@ void Document::onAutoresetRequested(int maxSize, bool query) // subsequent server command comes in too fast. sendLockSession(true); - m_client->sendMessage(drawdance::Message::makeChat( - m_client->myId(), DP_MSG_CHAT_TFLAGS_BYPASS, DP_MSG_CHAT_OFLAGS_ACTION, + m_client->sendMessage(net::makeChatMessage( + m_client->myId(), DP_MSG_CHAT_TFLAGS_BYPASS, + DP_MSG_CHAT_OFLAGS_ACTION, QStringLiteral("beginning session autoreset..."))); sendResetSession(); @@ -339,22 +377,22 @@ void Document::onAutoresetRequested(int maxSize, bool query) } } -void Document::onMoveLayerRequested(int sourceId, int targetId, bool intoGroup, bool below) +void Document::onMoveLayerRequested( + int sourceId, int targetId, bool intoGroup, bool below) { uint8_t contextId = m_client->myId(); - drawdance::Message msg; + net::Message msg; if(m_client->isCompatibilityMode()) { - msg = m_canvas->paintEngine()->historyCanvasState() - .makeLayerOrder(contextId, sourceId, targetId, below); + msg = m_canvas->paintEngine()->historyCanvasState().makeLayerOrder( + contextId, sourceId, targetId, below); } else { - msg = m_canvas->paintEngine()->historyCanvasState() - .makeLayerTreeMove(contextId, sourceId, targetId, intoGroup, below); + msg = m_canvas->paintEngine()->historyCanvasState().makeLayerTreeMove( + contextId, sourceId, targetId, intoGroup, below); } if(msg.isNull()) { qWarning("Can't move layer: %s", DP_error()); } else { - drawdance::Message messages[] = { - drawdance::Message::makeUndoPoint(contextId), msg}; + net::Message messages[] = {net::makeUndoPointMessage(contextId), msg}; m_client->sendMessages(DP_ARRAY_LENGTH(messages), messages); } } @@ -397,7 +435,7 @@ void Document::setSessionResetThreshold(int threshold) // if a low hard size limit is in place. This ensures the settings dialog // value is always up to date. m_sessionResetThreshold = threshold; - emit sessionResetThresholdChanged(threshold / double(1024*1024)); + emit sessionResetThresholdChanged(threshold / double(1024 * 1024)); } void Document::setBaseResetThreshold(int threshold) @@ -520,7 +558,8 @@ void Document::setWantCanvasHistoryDump(bool wantCanvasHistoryDump) { m_wantCanvasHistoryDump = wantCanvasHistoryDump; if(m_canvas) { - m_canvas->paintEngine()->setWantCanvasHistoryDump(wantCanvasHistoryDump); + m_canvas->paintEngine()->setWantCanvasHistoryDump( + wantCanvasHistoryDump); } } @@ -557,7 +596,8 @@ void Document::autosaveNow() void Document::saveCanvasAs(const QString &filename) { - saveCanvasStateAs(filename, m_canvas->paintEngine()->viewCanvasState(), true); + saveCanvasStateAs( + filename, m_canvas->paintEngine()->viewCanvasState(), true); } void Document::saveCanvasStateAs( @@ -568,25 +608,30 @@ void Document::saveCanvasStateAs( saveCanvasState(canvasState, isCurrentState); } -void Document::saveCanvasState(const drawdance::CanvasState &canvasState, bool isCurrentState) +void Document::saveCanvasState( + const drawdance::CanvasState &canvasState, bool isCurrentState) { Q_ASSERT(!m_saveInProgress); m_saveInProgress = true; - CanvasSaverRunnable *saver = new CanvasSaverRunnable(canvasState, m_currentFilename); + CanvasSaverRunnable *saver = + new CanvasSaverRunnable(canvasState, m_currentFilename); if(isCurrentState) { unmarkDirty(); } - connect(saver, &CanvasSaverRunnable::saveComplete, this, &Document::onCanvasSaved); + connect( + saver, &CanvasSaverRunnable::saveComplete, this, + &Document::onCanvasSaved); emit canvasSaveStarted(); QThreadPool::globalInstance()->start(saver); } void Document::exportTemplate(const QString &path) { - drawdance::MessageList snapshot = m_canvas->generateSnapshot( + net::MessageList snapshot = m_canvas->generateSnapshot( false, DP_ACL_STATE_RESET_IMAGE_TEMPLATE_FLAGS); - drawdance::RecordStartResult result = m_canvas->paintEngine()->exportTemplate(path, snapshot); + drawdance::RecordStartResult result = + m_canvas->paintEngine()->exportTemplate(path, snapshot); QString errorMessage; switch(result) { case drawdance::RECORD_START_SUCCESS: @@ -645,30 +690,33 @@ bool Document::isRecording() const void Document::sendPointerMove(const QPointF &point) { - m_client->sendMessage(drawdance::Message::makeMovePointer( + m_client->sendMessage(net::makeMovePointerMessage( m_client->myId(), point.x() * 4, point.y() * 4)); } void Document::sendSessionConf(const QJsonObject &sessionconf) { - m_client->sendMessage(net::ServerCommand::make("sessionconf", QJsonArray(), sessionconf)); + m_client->sendMessage( + net::ServerCommand::make("sessionconf", QJsonArray(), sessionconf)); } -void Document::sendFeatureAccessLevelChange(const uint8_t tiers[DP_FEATURE_COUNT]) +void Document::sendFeatureAccessLevelChange( + const uint8_t tiers[DP_FEATURE_COUNT]) { - m_client->sendMessage(drawdance::Message::makeFeatureAccessLevels( + m_client->sendMessage(net::makeFeatureAccessLevelsMessage( m_client->myId(), DP_FEATURE_COUNT, tiers)); } void Document::sendLockSession(bool lock) { - m_client->sendMessage(drawdance::Message::makeLayerAcl( + m_client->sendMessage(net::makeLayerAclMessage( m_client->myId(), 0, lock ? DP_ACL_ALL_LOCKED_BIT : 0, {})); } void Document::sendOpword(const QString &opword) { - m_client->sendMessage(net::ServerCommand::make("gain-op", QJsonArray() << opword)); + m_client->sendMessage( + net::ServerCommand::make("gain-op", QJsonArray() << opword)); } /** @@ -677,13 +725,15 @@ void Document::sendOpword(const QString &opword) * The reset image will be sent when the server acknowledges the request. * If an empty reset image is given here, one will be generated just in time. * - * If the document is in offline mode, this will immediately reset the current canvas. + * If the document is in offline mode, this will immediately reset the current + * canvas. */ -void Document::sendResetSession(const drawdance::MessageList &resetImage) +void Document::sendResetSession(const net::MessageList &resetImage) { if(!m_client->isConnected()) { if(resetImage.isEmpty()) { - qWarning("Tried to do an offline session reset with a blank reset image"); + qWarning("Tried to do an offline session reset with a blank reset " + "image"); return; } // Not connected? Do a local reset @@ -695,7 +745,9 @@ void Document::sendResetSession(const drawdance::MessageList &resetImage) if(resetImage.isEmpty()) { qInfo("Sending session reset request. (Just in time snapshot)"); } else { - qInfo("Sending session reset request. (Snapshot size is %lld bytes)", compat::cast(resetImage.length())); + qInfo( + "Sending session reset request. (Snapshot size is %lld bytes)", + compat::cast(resetImage.length())); } m_resetstate = resetImage; @@ -705,9 +757,9 @@ void Document::sendResetSession(const drawdance::MessageList &resetImage) void Document::sendResizeCanvas(int top, int right, int bottom, int left) { uint8_t contextId = m_client->myId(); - drawdance::Message msgs[] = { - drawdance::Message::makeUndoPoint(contextId), - drawdance::Message::makeCanvasResize(contextId, top, right, bottom, left), + net::Message msgs[] = { + net::makeUndoPointMessage(contextId), + net::makeCanvasResizeMessage(contextId, top, right, bottom, left), }; m_client->sendMessages(DP_ARRAY_LENGTH(msgs), msgs); } @@ -735,9 +787,9 @@ void Document::sendTerminateSession() void Document::sendCanvasBackground(const QColor &color) { uint8_t contextId = m_client->myId(); - drawdance::Message msgs[] = { - drawdance::Message::makeUndoPoint(contextId), - drawdance::Message::makeCanvasBackground(contextId, color), + net::Message msgs[] = { + net::makeUndoPointMessage(contextId), + net::makeCanvasBackgroundMessage(contextId, color), }; m_client->sendMessages(DP_ARRAY_LENGTH(msgs), msgs); } @@ -748,7 +800,8 @@ void Document::sendAbuseReport(int userId, const QString &message) if(userId > 0 && userId < 256) kwargs["user"] = userId; kwargs["reason"] = message; - m_client->sendMessage(net::ServerCommand::make("report", QJsonArray(), kwargs)); + m_client->sendMessage( + net::ServerCommand::make("report", QJsonArray(), kwargs)); } void Document::snapshotNeeded() @@ -757,7 +810,7 @@ void Document::snapshotNeeded() if(m_canvas) { if(m_resetstate.isEmpty()) { // FunctionRunnable has autoDelete enabled - auto runnable = new utils::FunctionRunnable([this](){ + auto runnable = new utils::FunctionRunnable([this]() { generateJustInTimeSnapshot(); }); QThreadPool::globalInstance()->start(runnable); @@ -765,7 +818,8 @@ void Document::snapshotNeeded() sendResetSnapshot(); } } else { - qWarning("Server requested snapshot, but canvas is not yet initialized!"); + qWarning( + "Server requested snapshot, but canvas is not yet initialized!"); m_client->sendMessage(net::ServerCommand::make("init-cancel")); } } @@ -786,14 +840,19 @@ void Document::generateJustInTimeSnapshot() void Document::sendResetSnapshot() { // Size limit check. The server will kick us if we send an oversized reset. - if(m_sessionHistoryMaxSize > 0 && m_resetstate.length() > m_sessionHistoryMaxSize) { - qWarning("Reset snapshot (%lld) is larger than the size limit (%d)!", compat::cast(m_resetstate.length()), m_sessionHistoryMaxSize); + if(m_sessionHistoryMaxSize > 0 && + m_resetstate.length() > m_sessionHistoryMaxSize) { + qWarning( + "Reset snapshot (%lld) is larger than the size limit (%d)!", + compat::cast(m_resetstate.length()), + m_sessionHistoryMaxSize); emit autoResetTooLarge(m_sessionHistoryMaxSize); m_client->sendMessage(net::ServerCommand::make("init-cancel")); } else { // Send the reset command+image m_client->sendResetMessage(net::ServerCommand::make("init-begin")); - m_client->sendResetMessages(m_resetstate.count(), m_resetstate.constData()); + m_client->sendResetMessages( + m_resetstate.count(), m_resetstate.constData()); m_client->sendResetMessage(net::ServerCommand::make("init-complete")); } m_resetstate.clear(); @@ -805,8 +864,7 @@ void Document::undo() return; if(!m_toolctrl->undoMultipartDrawing()) { - m_client->sendMessage( - drawdance::Message::makeUndo(m_client->myId(), 0, false)); + m_client->sendMessage(net::makeUndoMessage(m_client->myId(), 0, false)); } } @@ -817,8 +875,7 @@ void Document::redo() // Cannot redo while a multipart drawing action is in progress if(!m_toolctrl->isMultipartDrawing()) { - m_client->sendMessage( - drawdance::Message::makeUndo(m_client->myId(), 0, true)); + m_client->sendMessage(net::makeUndoMessage(m_client->myId(), 0, true)); } } @@ -835,10 +892,13 @@ void Document::selectAll() void Document::selectNone() { - if(m_canvas && m_canvas->selection() && m_canvas->selection()->pasteOrMoveToCanvas( - m_messageBuffer, m_client->myId(), m_toolctrl->activeLayer(), - m_toolctrl->selectInterpolation(), m_client->isCompatibilityMode())) { - m_client->sendMessages(m_messageBuffer.count(), m_messageBuffer.constData()); + if(m_canvas && m_canvas->selection() && + m_canvas->selection()->pasteOrMoveToCanvas( + m_messageBuffer, m_client->myId(), m_toolctrl->activeLayer(), + m_toolctrl->selectInterpolation(), + m_client->isCompatibilityMode())) { + m_client->sendMessages( + m_messageBuffer.count(), m_messageBuffer.constData()); m_messageBuffer.clear(); } cancelSelection(); @@ -865,21 +925,17 @@ void Document::copyFromLayer(int layer) QPoint srcpos; if(m_canvas->selection()) { const auto br = m_canvas->selection()->boundingRect(); - srcpos = br.topLeft() + QPoint { - img.width() / 2, - img.height() / 2 - }; + srcpos = br.topLeft() + QPoint{img.width() / 2, img.height() / 2}; } else { const QSize s = m_canvas->size(); - srcpos = QPoint(s.width()/2, s.height()/2); + srcpos = QPoint(s.width() / 2, s.height() / 2); } - QByteArray srcbuf = - QByteArray::number(srcpos.x()) + "," + - QByteArray::number(srcpos.y()) + "," + - QByteArray::number(qApp->applicationPid()) + "," + - QByteArray::number(pasteId()); + QByteArray srcbuf = QByteArray::number(srcpos.x()) + "," + + QByteArray::number(srcpos.y()) + "," + + QByteArray::number(qApp->applicationPid()) + "," + + QByteArray::number(pasteId()); data->setData("x-drawpile/pastesrc", srcbuf); QGuiApplication::clipboard()->setMimeData(data); @@ -926,7 +982,8 @@ void Document::cutLayer() } } -void Document::pasteImage(const QImage &image, const QPoint &point, bool forcePoint) +void Document::pasteImage( + const QImage &image, const QPoint &point, bool forcePoint) { if(m_canvas) { m_canvas->pasteFromImage(image, point, forcePoint); @@ -936,10 +993,13 @@ void Document::pasteImage(const QImage &image, const QPoint &point, bool forcePo void Document::stamp() { canvas::Selection *sel = m_canvas ? m_canvas->selection() : nullptr; - if(sel && !sel->pasteImage().isNull() && sel->pasteOrMoveToCanvas( - m_messageBuffer, m_client->myId(), m_toolctrl->activeLayer(), - m_toolctrl->selectInterpolation(), m_client->isCompatibilityMode())) { - m_client->sendMessages(m_messageBuffer.count(), m_messageBuffer.constData()); + if(sel && !sel->pasteImage().isNull() && + sel->pasteOrMoveToCanvas( + m_messageBuffer, m_client->myId(), m_toolctrl->activeLayer(), + m_toolctrl->selectInterpolation(), + m_client->isCompatibilityMode())) { + m_client->sendMessages( + m_messageBuffer.count(), m_messageBuffer.constData()); m_messageBuffer.clear(); sel->detachMove(); } @@ -966,10 +1026,13 @@ void Document::fillArea(const QColor &color, DP_BlendMode mode, bool source) qWarning("fillArea: no canvas!"); return; } - if(m_canvas->selection() && !m_canvas->aclState()->isLayerLocked(m_toolctrl->activeLayer()) - && m_canvas->selection()->fillCanvas( - m_messageBuffer, m_client->myId(), color, mode, m_toolctrl->activeLayer(), source)) { - m_client->sendMessages(m_messageBuffer.count(), m_messageBuffer.constData()); + if(m_canvas->selection() && + !m_canvas->aclState()->isLayerLocked(m_toolctrl->activeLayer()) && + m_canvas->selection()->fillCanvas( + m_messageBuffer, m_client->myId(), color, mode, + m_toolctrl->activeLayer(), source)) { + m_client->sendMessages( + m_messageBuffer.count(), m_messageBuffer.constData()); m_messageBuffer.clear(); } } @@ -982,17 +1045,19 @@ void Document::removeEmptyAnnotations() } uint8_t contextId = m_canvas->localUserId(); - drawdance::AnnotationList al = m_canvas->paintEngine()->historyCanvasState().annotations(); + drawdance::AnnotationList al = + m_canvas->paintEngine()->historyCanvasState().annotations(); int count = al.count(); for(int i = 0; i < count; ++i) { drawdance::Annotation a = al.at(i); if(a.textBytes().length() == 0) { m_messageBuffer.append( - drawdance::Message::makeAnnotationDelete(contextId, a.id())); + net::makeAnnotationDeleteMessage(contextId, a.id())); } } - m_client->sendMessages(m_messageBuffer.count(), m_messageBuffer.constData()); + m_client->sendMessages( + m_messageBuffer.count(), m_messageBuffer.constData()); m_messageBuffer.clear(); } @@ -1002,4 +1067,3 @@ void Document::addServerLogEntry(const QString &log) m_serverLog->insertRow(i); m_serverLog->setData(m_serverLog->index(i), log); } - diff --git a/src/libclient/document.h b/src/libclient/document.h index c71fc3b19e..8394ed69b7 100644 --- a/src/libclient/document.h +++ b/src/libclient/document.h @@ -1,20 +1,16 @@ // SPDX-License-Identifier: GPL-3.0-or-later - #ifndef DRAWPILE_DOCUMENT_H #define DRAWPILE_DOCUMENT_H - extern "C" { +#include #include #include -#include } - -#include "libclient/drawdance/message.h" #include "libclient/drawdance/paintengine.h" #include "libclient/net/announcementlist.h" #include "libclient/net/banlistmodel.h" +#include "libclient/net/message.h" #include "libshared/util/qtcompat.h" - #include #include @@ -22,59 +18,84 @@ class QString; class QTimer; namespace canvas { - class CanvasModel; +class CanvasModel; } namespace net { - class Client; - class BanlistModel; - class AnnouncementListModel; +class Client; +class BanlistModel; +class AnnouncementListModel; +} +namespace libclient { +namespace settings { +class Settings; +} +} +namespace tools { +class ToolController; } -namespace libclient { namespace settings { class Settings; } } -namespace tools { class ToolController; } /** - * @brief An active document and its associated data, including the network connection + * @brief An active document and its associated data, including the network + * connection * * This is an UI agnostic class that should be usable from both a widget * based application or a pure QML app. * */ -class Document final : public QObject -{ - Q_PROPERTY(canvas::CanvasModel* canvas READ canvas() NOTIFY canvasChanged) - Q_PROPERTY(net::BanlistModel* banlist READ banlist() CONSTANT) - Q_PROPERTY(net::AnnouncementListModel* announcementList READ announcementList() CONSTANT) - Q_PROPERTY(QStringListModel* serverLog READ serverLog() CONSTANT) +class Document final : public QObject { + Q_PROPERTY(canvas::CanvasModel *canvas READ canvas() NOTIFY canvasChanged) + Q_PROPERTY(net::BanlistModel *banlist READ banlist() CONSTANT) + Q_PROPERTY(net::AnnouncementListModel *announcementList READ + announcementList() CONSTANT) + Q_PROPERTY(QStringListModel *serverLog READ serverLog() CONSTANT) Q_PROPERTY(bool dirty READ isDirty NOTIFY dirtyCanvas) - Q_PROPERTY(bool autosave READ isAutosave WRITE setAutosave NOTIFY autosaveChanged) + Q_PROPERTY( + bool autosave READ isAutosave WRITE setAutosave NOTIFY autosaveChanged) Q_PROPERTY(bool canAutosave READ canAutosave NOTIFY canAutosaveChanged) - Q_PROPERTY(QString sessionTitle READ sessionTitle NOTIFY sessionTitleChanged) - Q_PROPERTY(QString currentFilename READ currentFilename() NOTIFY currentFilenameChanged) + Q_PROPERTY( + QString sessionTitle READ sessionTitle NOTIFY sessionTitleChanged) + Q_PROPERTY(QString currentFilename READ currentFilename() + NOTIFY currentFilenameChanged) Q_PROPERTY(bool recording READ isRecording() NOTIFY recorderStateChanged) - Q_PROPERTY(bool sessionPersistent READ isSessionPersistent NOTIFY sessionPersistentChanged) - Q_PROPERTY(bool sessionClosed READ isSessionClosed NOTIFY sessionClosedChanged) - Q_PROPERTY(bool sessionAuthOnly READ isSessionAuthOnly NOTIFY sessionAuthOnlyChanged) - Q_PROPERTY(bool sessionPreserveChat READ isSessionPreserveChat NOTIFY sessionPreserveChatChanged) - Q_PROPERTY(bool sessionPasswordProtected READ isSessionPasswordProtected NOTIFY sessionPasswordChanged) - Q_PROPERTY(bool sessionHasOpword READ isSessionOpword NOTIFY sessionOpwordChanged) + Q_PROPERTY(bool sessionPersistent READ isSessionPersistent NOTIFY + sessionPersistentChanged) + Q_PROPERTY( + bool sessionClosed READ isSessionClosed NOTIFY sessionClosedChanged) + Q_PROPERTY(bool sessionAuthOnly READ isSessionAuthOnly NOTIFY + sessionAuthOnlyChanged) + Q_PROPERTY(bool sessionPreserveChat READ isSessionPreserveChat NOTIFY + sessionPreserveChatChanged) + Q_PROPERTY(bool sessionPasswordProtected READ isSessionPasswordProtected + NOTIFY sessionPasswordChanged) + Q_PROPERTY( + bool sessionHasOpword READ isSessionOpword NOTIFY sessionOpwordChanged) Q_PROPERTY(bool sessionNsfm READ isSessionNsfm NOTIFY sessionNsfmChanged) - Q_PROPERTY(bool sessionForceNsfm READ isSessionForceNsfm NOTIFY sessionForceNsfmChanged) - Q_PROPERTY(bool sessionDeputies READ isSessionDeputies NOTIFY sessionDeputiesChanged) - Q_PROPERTY(int sessionMaxUserCount READ sessionMaxUserCount NOTIFY sessionMaxUserCountChanged) - Q_PROPERTY(double sessionResetThreshold READ sessionResetThreshold NOTIFY sessionResetThresholdChanged) - Q_PROPERTY(double baseResetThreshold READ baseResetThreshold NOTIFY baseResetThresholdChanged) + Q_PROPERTY(bool sessionForceNsfm READ isSessionForceNsfm NOTIFY + sessionForceNsfmChanged) + Q_PROPERTY(bool sessionDeputies READ isSessionDeputies NOTIFY + sessionDeputiesChanged) + Q_PROPERTY(int sessionMaxUserCount READ sessionMaxUserCount NOTIFY + sessionMaxUserCountChanged) + Q_PROPERTY(double sessionResetThreshold READ sessionResetThreshold NOTIFY + sessionResetThresholdChanged) + Q_PROPERTY(double baseResetThreshold READ baseResetThreshold NOTIFY + baseResetThresholdChanged) Q_PROPERTY(QString roomcode READ roomcode NOTIFY sessionRoomcodeChanged) Q_OBJECT public: - explicit Document(libclient::settings::Settings &settings, QObject *parent=nullptr); + explicit Document( + libclient::settings::Settings &settings, QObject *parent = nullptr); canvas::CanvasModel *canvas() const { return m_canvas; } tools::ToolController *toolCtrl() const { return m_toolctrl; } net::Client *client() const { return m_client; } net::BanlistModel *banlist() const { return m_banlist; } - net::AnnouncementListModel *announcementList() const { return m_announcementlist; } + net::AnnouncementListModel *announcementList() const + { + return m_announcementlist; + } QStringListModel *serverLog() const { return m_serverLog; } /** @@ -127,18 +148,30 @@ class Document final : public QObject bool isSessionClosed() const { return m_sessionClosed; } bool isSessionAuthOnly() const { return m_sessionAuthOnly; } bool isSessionPreserveChat() const { return m_sessionPreserveChat; } - bool isSessionPasswordProtected() const { return m_sessionPasswordProtected; } + bool isSessionPasswordProtected() const + { + return m_sessionPasswordProtected; + } bool isSessionOpword() const { return m_sessionOpword; } bool isSessionNsfm() const { return m_sessionNsfm; } bool isSessionForceNsfm() const { return m_sessionForceNsfm; } bool isSessionDeputies() const { return m_sessionDeputies; } int sessionMaxUserCount() const { return m_sessionMaxUserCount; } - double sessionResetThreshold() const { return m_sessionResetThreshold/double(1024*1024); } - double baseResetThreshold() const { return m_baseResetThreshold/double(1024*1024); } + double sessionResetThreshold() const + { + return m_sessionResetThreshold / double(1024 * 1024); + } + double baseResetThreshold() const + { + return m_baseResetThreshold / double(1024 * 1024); + } QString roomcode() const { return m_roomcode; } - void setRecordOnConnect(const QString &filename) { m_recordOnConnect = filename; } + void setRecordOnConnect(const QString &filename) + { + m_recordOnConnect = filename; + } qulonglong pasteId() const { return reinterpret_cast(this); } @@ -148,7 +181,8 @@ class Document final : public QObject //! Connection opened, but not yet logged in void serverConnected(const QString &address, int port); void serverLoggedIn(bool join, const QString &joinPassword); - void serverDisconnected(const QString &message, const QString &errorcode, bool localDisconnect); + void serverDisconnected( + const QString &message, const QString &errorcode, bool localDisconnect); void compatibilityModeChanged(bool compatibilityMode); void canvasChanged(canvas::CanvasModel *canvas); @@ -188,9 +222,9 @@ public slots: void sendPointerMove(const QPointF &point); void sendSessionConf(const QJsonObject &sessionconf); void sendFeatureAccessLevelChange(const uint8_t[DP_FEATURE_COUNT]); - void sendLockSession(bool lock=true); + void sendLockSession(bool lock = true); void sendOpword(const QString &opword); - void sendResetSession(const drawdance::MessageList &resetImage = {}); + void sendResetSession(const net::MessageList &resetImage = {}); void sendResizeCanvas(int top, int right, int bottom, int left); void sendUnban(int entryId); void sendAnnounce(const QString &url, bool privateMode); @@ -203,7 +237,8 @@ public slots: void undo(); void redo(); - void selectAll(); // Note: selection tool should be activated before calling this + void + selectAll(); // Note: selection tool should be activated before calling this void selectNone(); void cancelSelection(); @@ -211,7 +246,10 @@ public slots: void copyMerged(); void copyLayer(); void cutLayer(); - void pasteImage(const QImage &image, const QPoint &point, bool forcePoint); // Note: selection tool should be activated before calling this + void pasteImage( + const QImage &image, const QPoint &point, + bool forcePoint); // Note: selection tool should be activated before + // calling this void stamp(); void removeEmptyAnnotations(); @@ -221,13 +259,15 @@ public slots: void addServerLogEntry(const QString &log); private slots: - void onServerLogin(bool join, bool compatibilityMode, const QString &joinPassword); + void onServerLogin( + bool join, bool compatibilityMode, const QString &joinPassword); void onServerDisconnect(); void onSessionResetted(); void onSessionConfChanged(const QJsonObject &config); void onAutoresetRequested(int maxSize, bool query); - void onMoveLayerRequested(int sourceId, int targetId, bool intoGroup, bool below); + void onMoveLayerRequested( + int sourceId, int targetId, bool intoGroup, bool below); void snapshotNeeded(); void markDirty(); @@ -237,7 +277,8 @@ private slots: void onCanvasSaved(const QString &errorMessage); private: - void saveCanvasState(const drawdance::CanvasState &canvasState, bool isCurrentState); + void saveCanvasState( + const drawdance::CanvasState &canvasState, bool isCurrentState); void setCurrentFilename(const QString &filename); void setSessionPersistent(bool p); void setSessionClosed(bool closed); @@ -262,8 +303,8 @@ private slots: QString m_currentFilename; - drawdance::MessageList m_resetstate; - drawdance::MessageList m_messageBuffer; + net::MessageList m_resetstate; + net::MessageList m_messageBuffer; canvas::CanvasModel *m_canvas; tools::ToolController *m_toolctrl; @@ -302,4 +343,3 @@ private slots: }; #endif // DOCUMENT_H - diff --git a/src/libclient/drawdance/aclstate.cpp b/src/libclient/drawdance/aclstate.cpp index f0be3f751d..95e955cc91 100644 --- a/src/libclient/drawdance/aclstate.cpp +++ b/src/libclient/drawdance/aclstate.cpp @@ -1,65 +1,64 @@ // SPDX-License-Identifier: GPL-3.0-or-later - #include "libclient/drawdance/aclstate.h" namespace drawdance { AclState::AclState() - : m_data(DP_acl_state_new()) + : m_data(DP_acl_state_new()) { } AclState::~AclState() { - DP_acl_state_free(m_data); + DP_acl_state_free(m_data); } DP_AclState *AclState::get() { - return m_data; + return m_data; } void AclState::reset(uint8_t localUserId) { - DP_acl_state_reset(m_data, localUserId); + DP_acl_state_reset(m_data, localUserId); } char *AclState::dump() const { - return DP_acl_state_dump(m_data); + return DP_acl_state_dump(m_data); } DP_UserAcls AclState::users() const { - return DP_acl_state_users(m_data); + return DP_acl_state_users(m_data); } DP_FeatureTiers AclState::featureTiers() const { - return DP_acl_state_feature_tiers(m_data); + return DP_acl_state_feature_tiers(m_data); } void AclState::eachLayerAcl(AclState::EachLayerFn fn) const { - DP_acl_state_layers_each(m_data, &AclState::onLayerAcl, &fn); + DP_acl_state_layers_each(m_data, &AclState::onLayerAcl, &fn); } void AclState::toResetImage( - MessageList &msgs, uint8_t userId, unsigned int includeFlags) const + net::MessageList &msgs, uint8_t userId, unsigned int includeFlags) const { - DP_acl_state_reset_image_build( - m_data, userId, includeFlags, pushMessage, &msgs); + DP_acl_state_reset_image_build( + m_data, userId, includeFlags, pushMessage, &msgs); } void AclState::onLayerAcl(void *user, int layerId, const DP_LayerAcl *layerAcl) { - (*static_cast(user))(layerId, layerAcl); + (*static_cast(user))(layerId, layerAcl); } bool AclState::pushMessage(void *user, DP_Message *msg) { - static_cast(user)->append(drawdance::Message::noinc(msg)); - return true; + static_cast(user)->append(net::Message::noinc(msg)); + return true; } } diff --git a/src/libclient/drawdance/aclstate.h b/src/libclient/drawdance/aclstate.h index 61fdf960c5..ffb308f42f 100644 --- a/src/libclient/drawdance/aclstate.h +++ b/src/libclient/drawdance/aclstate.h @@ -1,51 +1,50 @@ // SPDX-License-Identifier: GPL-3.0-or-later - #ifndef DRAWDANCE_ACLSTATE_H #define DRAWDANCE_ACLSTATE_H - extern "C" { #include } - -#include "libclient/drawdance/message.h" - -#include +#include "libclient/net/message.h" #include +#include namespace drawdance { class AclState { - using EachLayerFn = std::function; + using EachLayerFn = std::function; public: - AclState(); - ~AclState(); + AclState(); + ~AclState(); - AclState(const AclState &) = delete; - AclState(AclState &&) = delete; - AclState &operator=(const AclState &) = delete; - AclState &operator=(AclState &&) = delete; + AclState(const AclState &) = delete; + AclState(AclState &&) = delete; + AclState &operator=(const AclState &) = delete; + AclState &operator=(AclState &&) = delete; - DP_AclState *get(); + DP_AclState *get(); - void reset(uint8_t localUserId); + void reset(uint8_t localUserId); - char *dump() const; + char *dump() const; - DP_UserAcls users() const; + DP_UserAcls users() const; - DP_FeatureTiers featureTiers() const; + DP_FeatureTiers featureTiers() const; - void eachLayerAcl(EachLayerFn fn) const; + void eachLayerAcl(EachLayerFn fn) const; - void toResetImage(MessageList &msgs, uint8_t userId, unsigned int includeFlags) const; + void toResetImage( + net::MessageList &msgs, uint8_t userId, + unsigned int includeFlags) const; private: - static void onLayerAcl(void *user, int layerId, const DP_LayerAcl *layerAcl); + static void + onLayerAcl(void *user, int layerId, const DP_LayerAcl *layerAcl); - static bool pushMessage(void *user, DP_Message *msg); + static bool pushMessage(void *user, DP_Message *msg); - DP_AclState *m_data; + DP_AclState *m_data; }; } diff --git a/src/libclient/drawdance/brushengine.cpp b/src/libclient/drawdance/brushengine.cpp index 0b9e3b6a53..38c4dd498a 100644 --- a/src/libclient/drawdance/brushengine.cpp +++ b/src/libclient/drawdance/brushengine.cpp @@ -1,97 +1,99 @@ // SPDX-License-Identifier: GPL-3.0-or-later - extern "C" { #include #include } - +#include "libclient/canvas/point.h" #include "libclient/drawdance/brushengine.h" #include "libclient/drawdance/canvasstate.h" -#include "libclient/canvas/point.h" #include "libclient/net/client.h" namespace drawdance { BrushEngine::BrushEngine(PollControlFn pollControl) - : m_messages{} - , m_pollControl{pollControl} - , m_data{DP_brush_engine_new(&BrushEngine::pushMessage, pollControl ? &BrushEngine::pollControl : nullptr, this)} + : m_messages{} + , m_pollControl{pollControl} + , m_data{DP_brush_engine_new( + &BrushEngine::pushMessage, + pollControl ? &BrushEngine::pollControl : nullptr, this)} { } BrushEngine::~BrushEngine() { - DP_brush_engine_free(m_data); + DP_brush_engine_free(m_data); } void BrushEngine::setClassicBrush( - const DP_ClassicBrush &brush, const DP_StrokeParams &stroke) + const DP_ClassicBrush &brush, const DP_StrokeParams &stroke) { - DP_brush_engine_classic_brush_set(m_data, &brush, &stroke, nullptr); + DP_brush_engine_classic_brush_set(m_data, &brush, &stroke, nullptr); } void BrushEngine::setMyPaintBrush( - const DP_MyPaintBrush &brush, const DP_MyPaintSettings &settings, - const DP_StrokeParams &stroke) + const DP_MyPaintBrush &brush, const DP_MyPaintSettings &settings, + const DP_StrokeParams &stroke) { - DP_brush_engine_mypaint_brush_set( - m_data, &brush, &settings, &stroke, nullptr); + DP_brush_engine_mypaint_brush_set( + m_data, &brush, &settings, &stroke, nullptr); } void BrushEngine::flushDabs() { - DP_brush_engine_dabs_flush(m_data); + DP_brush_engine_dabs_flush(m_data); } -void BrushEngine::beginStroke(unsigned int contextId, bool pushUndoPoint, float zoom) +void BrushEngine::beginStroke( + unsigned int contextId, bool pushUndoPoint, float zoom) { - m_messages.clear(); - DP_brush_engine_stroke_begin(m_data, contextId, pushUndoPoint, zoom); + m_messages.clear(); + DP_brush_engine_stroke_begin(m_data, contextId, pushUndoPoint, zoom); } -void BrushEngine::strokeTo(const canvas::Point &point, const drawdance::CanvasState &cs) +void BrushEngine::strokeTo( + const canvas::Point &point, const drawdance::CanvasState &cs) { - DP_BrushPoint bp = { - float(point.x()), float(point.y()), float(point.pressure()), - float(point.xtilt()), float(point.ytilt()), float(point.rotation()), - point.timeMsec()}; - DP_brush_engine_stroke_to(m_data, bp, cs.get()); + DP_BrushPoint bp = {float(point.x()), float(point.y()), + float(point.pressure()), float(point.xtilt()), + float(point.ytilt()), float(point.rotation()), + point.timeMsec()}; + DP_brush_engine_stroke_to(m_data, bp, cs.get()); } void BrushEngine::poll(long long timeMsec, const drawdance::CanvasState &cs) { - DP_brush_engine_poll(m_data, timeMsec, cs.get()); + DP_brush_engine_poll(m_data, timeMsec, cs.get()); } void BrushEngine::endStroke( - long long timeMsec, const drawdance::CanvasState &cs, bool pushPenUp) + long long timeMsec, const drawdance::CanvasState &cs, bool pushPenUp) { - DP_brush_engine_stroke_end(m_data, timeMsec, cs.get(), pushPenUp); + DP_brush_engine_stroke_end(m_data, timeMsec, cs.get(), pushPenUp); } void BrushEngine::addOffset(float x, float y) { - DP_brush_engine_offset_add(m_data, x, y); + DP_brush_engine_offset_add(m_data, x, y); } void BrushEngine::sendMessagesTo(net::Client *client) { - Q_ASSERT(client); - flushDabs(); - client->sendMessages(m_messages.count(), m_messages.constData()); - clearMessages(); + Q_ASSERT(client); + flushDabs(); + client->sendMessages(m_messages.count(), m_messages.constData()); + clearMessages(); } void BrushEngine::pushMessage(void *user, DP_Message *msg) { - BrushEngine *brushEngine = static_cast(user); - brushEngine->m_messages.append(drawdance::Message::noinc(msg)); + BrushEngine *brushEngine = static_cast(user); + brushEngine->m_messages.append(net::Message::noinc(msg)); } void BrushEngine::pollControl(void *user, bool enable) { - BrushEngine *brushEngine = static_cast(user); - brushEngine->m_pollControl(enable); + BrushEngine *brushEngine = static_cast(user); + brushEngine->m_pollControl(enable); } } diff --git a/src/libclient/drawdance/brushengine.h b/src/libclient/drawdance/brushengine.h index 7119b3c340..d1a332afa7 100644 --- a/src/libclient/drawdance/brushengine.h +++ b/src/libclient/drawdance/brushengine.h @@ -1,13 +1,10 @@ // SPDX-License-Identifier: GPL-3.0-or-later - #ifndef DRAWDANCE_BRUSH_ENGINE_H #define DRAWDANCE_BRUSH_ENGINE_H - extern "C" { #include } - -#include "libclient/drawdance/message.h" +#include "libclient/net/message.h" #include struct DP_ClassicBrush; @@ -15,11 +12,11 @@ struct DP_MyPaintBrush; struct DP_MyPaintSettings; namespace canvas { - class Point; +class Point; } namespace net { - class Client; +class Client; } namespace drawdance { @@ -28,53 +25,52 @@ class CanvasState; class BrushEngine final { public: - using PollControlFn = std::function; + using PollControlFn = std::function; - BrushEngine(PollControlFn pollControl = nullptr); - ~BrushEngine(); + BrushEngine(PollControlFn pollControl = nullptr); + ~BrushEngine(); - BrushEngine(const BrushEngine &) = delete; - BrushEngine(BrushEngine &&) = delete; - BrushEngine &operator=(const BrushEngine &) = delete; - BrushEngine &operator=(BrushEngine &&) = delete; + BrushEngine(const BrushEngine &) = delete; + BrushEngine(BrushEngine &&) = delete; + BrushEngine &operator=(const BrushEngine &) = delete; + BrushEngine &operator=(BrushEngine &&) = delete; - void setClassicBrush( - const DP_ClassicBrush &brush, const DP_StrokeParams &stroke); + void setClassicBrush( + const DP_ClassicBrush &brush, const DP_StrokeParams &stroke); - void setMyPaintBrush( - const DP_MyPaintBrush &brush, const DP_MyPaintSettings &settings, - const DP_StrokeParams &stroke); + void setMyPaintBrush( + const DP_MyPaintBrush &brush, const DP_MyPaintSettings &settings, + const DP_StrokeParams &stroke); - void flushDabs(); + void flushDabs(); - const drawdance::MessageList &messages() const { return m_messages; } + const net::MessageList &messages() const { return m_messages; } - void clearMessages() { m_messages.clear(); } + void clearMessages() { m_messages.clear(); } - void beginStroke(unsigned int contextId, bool pushUndoPoint, float zoom); + void beginStroke(unsigned int contextId, bool pushUndoPoint, float zoom); - void strokeTo(const canvas::Point &point, const drawdance::CanvasState &cs); + void strokeTo(const canvas::Point &point, const drawdance::CanvasState &cs); - void poll(long long timeMsec, const drawdance::CanvasState &cs); + void poll(long long timeMsec, const drawdance::CanvasState &cs); - void endStroke( - long long timeMsec, const drawdance::CanvasState &cs, bool pushPenUp); + void endStroke( + long long timeMsec, const drawdance::CanvasState &cs, bool pushPenUp); - void addOffset(float x, float y); + void addOffset(float x, float y); - // Flushes dabs and sends accumulated messages to the client. - void sendMessagesTo(net::Client *client); + // Flushes dabs and sends accumulated messages to the client. + void sendMessagesTo(net::Client *client); private: - static void pushMessage(void *user, DP_Message *msg); - static void pollControl(void *user, bool enable); + static void pushMessage(void *user, DP_Message *msg); + static void pollControl(void *user, bool enable); - drawdance::MessageList m_messages; - PollControlFn m_pollControl; - DP_BrushEngine *m_data; + net::MessageList m_messages; + PollControlFn m_pollControl; + DP_BrushEngine *m_data; }; } #endif - diff --git a/src/libclient/drawdance/canvasstate.cpp b/src/libclient/drawdance/canvasstate.cpp index 67f3ab7f1a..49d46c3c8b 100644 --- a/src/libclient/drawdance/canvasstate.cpp +++ b/src/libclient/drawdance/canvasstate.cpp @@ -1,5 +1,4 @@ // SPDX-License-Identifier: GPL-3.0-or-later - extern "C" { #include #include @@ -12,305 +11,311 @@ extern "C" { #include #include } - #include "libclient/canvas/blendmodes.h" #include "libclient/drawdance/canvasstate.h" #include "libclient/drawdance/global.h" #include "libclient/drawdance/image.h" #include "libclient/drawdance/layergroup.h" #include "libclient/drawdance/layerprops.h" - #include namespace drawdance { CanvasState CanvasState::null() { - return CanvasState{nullptr}; + return CanvasState{nullptr}; } CanvasState CanvasState::inc(DP_CanvasState *cs) { - return CanvasState{DP_canvas_state_incref_nullable(cs)}; + return CanvasState{DP_canvas_state_incref_nullable(cs)}; } CanvasState CanvasState::noinc(DP_CanvasState *cs) { - return CanvasState{cs}; + return CanvasState{cs}; } CanvasState CanvasState::load(const QString &path, DP_LoadResult *outResult) { - QByteArray pathBytes = path.toUtf8(); - QByteArray flatImageLayerTitleBytes = QObject::tr("Layer %1").arg(1).toUtf8(); - DrawContext dc = DrawContextPool::acquire(); - DP_CanvasState *cs = DP_load( - dc.get(), pathBytes.constData(), flatImageLayerTitleBytes.constData(), outResult); - return CanvasState::noinc(cs); + QByteArray pathBytes = path.toUtf8(); + QByteArray flatImageLayerTitleBytes = + QObject::tr("Layer %1").arg(1).toUtf8(); + DrawContext dc = DrawContextPool::acquire(); + DP_CanvasState *cs = DP_load( + dc.get(), pathBytes.constData(), flatImageLayerTitleBytes.constData(), + outResult); + return CanvasState::noinc(cs); } CanvasState::CanvasState() - : CanvasState{nullptr} + : CanvasState{nullptr} { } CanvasState::CanvasState(const CanvasState &other) - : CanvasState{DP_canvas_state_incref_nullable(other.m_data)} + : CanvasState{DP_canvas_state_incref_nullable(other.m_data)} { } CanvasState::CanvasState(CanvasState &&other) - : CanvasState{other.m_data} + : CanvasState{other.m_data} { - other.m_data = nullptr; + other.m_data = nullptr; } CanvasState &CanvasState::operator=(const CanvasState &other) { - DP_canvas_state_decref_nullable(m_data); - m_data = DP_canvas_state_incref_nullable(other.m_data); - return *this; + DP_canvas_state_decref_nullable(m_data); + m_data = DP_canvas_state_incref_nullable(other.m_data); + return *this; } CanvasState &CanvasState::operator=(CanvasState &&other) { - DP_canvas_state_decref_nullable(m_data); - m_data = other.m_data; - other.m_data = nullptr; - return *this; + DP_canvas_state_decref_nullable(m_data); + m_data = other.m_data; + other.m_data = nullptr; + return *this; } CanvasState::~CanvasState() { - DP_canvas_state_decref_nullable(m_data); + DP_canvas_state_decref_nullable(m_data); } DP_CanvasState *CanvasState::get() const { - return m_data; + return m_data; } bool CanvasState::isNull() const { - return !m_data; + return !m_data; } int CanvasState::width() const { - return DP_canvas_state_width(m_data); + return DP_canvas_state_width(m_data); } int CanvasState::height() const { - return DP_canvas_state_height(m_data); + return DP_canvas_state_height(m_data); } QSize CanvasState::size() const { - return QSize{width(), height()}; + return QSize{width(), height()}; } int CanvasState::offsetX() const { - return DP_canvas_state_offset_x(m_data); + return DP_canvas_state_offset_x(m_data); } int CanvasState::offsetY() const { - return DP_canvas_state_offset_y(m_data); + return DP_canvas_state_offset_y(m_data); } QPoint CanvasState::offset() const { - return QPoint{offsetX(), offsetY()}; + return QPoint{offsetX(), offsetY()}; } Tile CanvasState::backgroundTile() const { - return Tile::inc(DP_canvas_state_background_tile_noinc(m_data)); + return Tile::inc(DP_canvas_state_background_tile_noinc(m_data)); } DocumentMetadata CanvasState::documentMetadata() const { - return DocumentMetadata::inc(DP_canvas_state_metadata_noinc(m_data)); + return DocumentMetadata::inc(DP_canvas_state_metadata_noinc(m_data)); } LayerList CanvasState::layers() const { - return LayerList::inc(DP_canvas_state_layers_noinc(m_data)); + return LayerList::inc(DP_canvas_state_layers_noinc(m_data)); } AnnotationList CanvasState::annotations() const { - return AnnotationList::inc(DP_canvas_state_annotations_noinc(m_data)); + return AnnotationList::inc(DP_canvas_state_annotations_noinc(m_data)); } Timeline CanvasState::timeline() const { - return Timeline::inc(DP_canvas_state_timeline_noinc(m_data)); + return Timeline::inc(DP_canvas_state_timeline_noinc(m_data)); } int CanvasState::frameCount() const { - return DP_canvas_state_frame_count(m_data); + return DP_canvas_state_frame_count(m_data); } int CanvasState::framerate() const { - return DP_canvas_state_framerate(m_data); + return DP_canvas_state_framerate(m_data); } bool CanvasState::sameFrame(int frameIndexA, int frameIndexB) const { - return DP_canvas_state_same_frame(m_data, frameIndexA, frameIndexB); + return DP_canvas_state_same_frame(m_data, frameIndexA, frameIndexB); } -QSet CanvasState::getLayersVisibleInTrackFrame(int trackId, int frameIndex) const +QSet +CanvasState::getLayersVisibleInTrackFrame(int trackId, int frameIndex) const { - QSet layersVisibleInTrackFrame; - DP_view_mode_get_layers_visible_in_track_frame( - m_data, trackId, frameIndex, addLayerVisibleInFrame, - &layersVisibleInTrackFrame); - return layersVisibleInTrackFrame; + QSet layersVisibleInTrackFrame; + DP_view_mode_get_layers_visible_in_track_frame( + m_data, trackId, frameIndex, addLayerVisibleInFrame, + &layersVisibleInTrackFrame); + return layersVisibleInTrackFrame; } QImage CanvasState::toFlatImage( - bool includeBackground, bool includeSublayers, - const QRect *rect, const DP_ViewModeFilter *vmf) const -{ - unsigned int flags = - (includeBackground ? DP_FLAT_IMAGE_INCLUDE_BACKGROUND : 0) | - (includeSublayers ? DP_FLAT_IMAGE_INCLUDE_SUBLAYERS : 0); - DP_Rect area; - if(rect) { - area = DP_rect_make(rect->x(), rect->y(), rect->width(), rect->height()); - } - DP_Image *img = DP_canvas_state_to_flat_image( - m_data, flags, rect ? &area : nullptr, vmf); - return wrapImage(img); + bool includeBackground, bool includeSublayers, const QRect *rect, + const DP_ViewModeFilter *vmf) const +{ + unsigned int flags = + (includeBackground ? DP_FLAT_IMAGE_INCLUDE_BACKGROUND : 0) | + (includeSublayers ? DP_FLAT_IMAGE_INCLUDE_SUBLAYERS : 0); + DP_Rect area; + if(rect) { + area = + DP_rect_make(rect->x(), rect->y(), rect->width(), rect->height()); + } + DP_Image *img = DP_canvas_state_to_flat_image( + m_data, flags, rect ? &area : nullptr, vmf); + return wrapImage(img); } QImage CanvasState::layerToFlatImage(int layerId, const QRect &rect) const { - DP_LayerRoutes *lr = DP_canvas_state_layer_routes_noinc(m_data); - DP_LayerRoutesEntry *lre = DP_layer_routes_search(lr, layerId); - if(lre) { - if(DP_layer_routes_entry_is_group(lre)) { - DP_LayerGroup *lg = DP_layer_routes_entry_group(lre, m_data); - DP_LayerProps *lp = DP_layer_routes_entry_props(lre, m_data); - return LayerGroup::inc(lg).toImage(LayerProps::inc(lp), rect); - } else { - DP_LayerContent *lc = DP_layer_routes_entry_content(lre, m_data); - return LayerContent::inc(lc).toImage(rect); - } - } else { - return QImage{}; - } + DP_LayerRoutes *lr = DP_canvas_state_layer_routes_noinc(m_data); + DP_LayerRoutesEntry *lre = DP_layer_routes_search(lr, layerId); + if(lre) { + if(DP_layer_routes_entry_is_group(lre)) { + DP_LayerGroup *lg = DP_layer_routes_entry_group(lre, m_data); + DP_LayerProps *lp = DP_layer_routes_entry_props(lre, m_data); + return LayerGroup::inc(lg).toImage(LayerProps::inc(lp), rect); + } else { + DP_LayerContent *lc = DP_layer_routes_entry_content(lre, m_data); + return LayerContent::inc(lc).toImage(rect); + } + } else { + return QImage{}; + } } -void CanvasState::toResetImage(MessageList &msgs, uint8_t contextId) const +void CanvasState::toResetImage(net::MessageList &msgs, uint8_t contextId) const { - DP_reset_image_build(m_data, contextId, &CanvasState::pushMessage, &msgs); + DP_reset_image_build(m_data, contextId, &CanvasState::pushMessage, &msgs); } -drawdance::Message CanvasState::makeLayerOrder( - uint8_t contextId, int sourceId, int targetId, bool below) const +net::Message CanvasState::makeLayerOrder( + uint8_t contextId, int sourceId, int targetId, bool below) const { - return drawdance::Message::noinc(DP_layer_routes_layer_order_make( + return net::Message::noinc(DP_layer_routes_layer_order_make( m_data, contextId, sourceId, targetId, below)); } -drawdance::Message CanvasState::makeLayerTreeMove( - uint8_t contextId, int sourceId, int targetId, bool intoGroup, bool below) const +net::Message CanvasState::makeLayerTreeMove( + uint8_t contextId, int sourceId, int targetId, bool intoGroup, + bool below) const { - DP_LayerRoutes *lr = DP_canvas_state_layer_routes_noinc(m_data); - return drawdance::Message::noinc(DP_layer_routes_layer_tree_move_make( + DP_LayerRoutes *lr = DP_canvas_state_layer_routes_noinc(m_data); + return net::Message::noinc(DP_layer_routes_layer_tree_move_make( lr, m_data, contextId, sourceId, targetId, intoGroup, below)); } LayerContent CanvasState::searchLayerContent(int layerId) const { - DP_LayerRoutes *lr = DP_canvas_state_layer_routes_noinc(m_data); - DP_LayerRoutesEntry *lre = DP_layer_routes_search(lr, layerId); - if(lre && !DP_layer_routes_entry_is_group(lre)) { - return LayerContent::inc(DP_layer_routes_entry_content(lre, m_data)); - } else { - return LayerContent::null(); - } + DP_LayerRoutes *lr = DP_canvas_state_layer_routes_noinc(m_data); + DP_LayerRoutesEntry *lre = DP_layer_routes_search(lr, layerId); + if(lre && !DP_layer_routes_entry_is_group(lre)) { + return LayerContent::inc(DP_layer_routes_entry_content(lre, m_data)); + } else { + return LayerContent::null(); + } } DP_FloodFillResult CanvasState::floodFill( - int x, int y, const QColor &fillColor, double tolerance, int layerId, - int sizeLimit, int gap, int expand, int featherRadius, - const QAtomicInt &cancel, QImage &outImg, int &outX, int &outY) const + int x, int y, const QColor &fillColor, double tolerance, int layerId, + int sizeLimit, int gap, int expand, int featherRadius, + const QAtomicInt &cancel, QImage &outImg, int &outX, int &outY) const { DP_UPixelFloat fillPixel = DP_upixel_float_from_color(fillColor.rgba()); DP_Image *img; - DP_FloodFillResult result = DP_flood_fill( + DP_FloodFillResult result = DP_flood_fill( m_data, x, y, fillPixel, tolerance, layerId, sizeLimit, gap, expand, - featherRadius, &img, &outX, &outY, shouldCancelFloodFill, - const_cast(&cancel)); - if(result == DP_FLOOD_FILL_SUCCESS) { - outImg = wrapImage(img); - } - return result; + featherRadius, &img, &outX, &outY, shouldCancelFloodFill, + const_cast(&cancel)); + if(result == DP_FLOOD_FILL_SUCCESS) { + outImg = wrapImage(img); + } + return result; } drawdance::CanvasState CanvasState::makeBackwardCompatible() const { - if(!m_data) { - return null(); - } - - DP_TransientCanvasState *tcs = DP_transient_canvas_state_new(m_data); - DP_TransientLayerList *tll = DP_transient_canvas_state_transient_layers(tcs, 0); - DP_TransientLayerPropsList *tlpl = DP_transient_canvas_state_transient_layer_props(tcs, 0); - int count = DP_transient_layer_list_count(tll); - for(int i = 0; i < count; ++i) { - DP_LayerProps *lp = DP_transient_layer_props_list_at_noinc(tlpl, i); - bool isGroup = DP_layer_props_children_noinc(lp); - bool isModeIncompatible = !canvas::blendmode::isBackwardCompatibleMode( - DP_BlendMode(DP_layer_props_blend_mode(lp))); - - if(isGroup) { - DP_transient_layer_list_merge_at(tll, lp, i); - DP_transient_layer_props_list_merge_at(tlpl, i); - } - - if(isModeIncompatible) { - DP_TransientLayerProps *tlp = - DP_transient_layer_props_list_transient_at_noinc(tlpl, i); - DP_transient_layer_props_blend_mode_set(tlp, DP_BLEND_MODE_NORMAL); - } - } - - // Other stuff like the timeline or document metadata will just get dropped. - return drawdance::CanvasState::noinc(DP_transient_canvas_state_persist(tcs)); + if(!m_data) { + return null(); + } + + DP_TransientCanvasState *tcs = DP_transient_canvas_state_new(m_data); + DP_TransientLayerList *tll = + DP_transient_canvas_state_transient_layers(tcs, 0); + DP_TransientLayerPropsList *tlpl = + DP_transient_canvas_state_transient_layer_props(tcs, 0); + int count = DP_transient_layer_list_count(tll); + for(int i = 0; i < count; ++i) { + DP_LayerProps *lp = DP_transient_layer_props_list_at_noinc(tlpl, i); + bool isGroup = DP_layer_props_children_noinc(lp); + bool isModeIncompatible = !canvas::blendmode::isBackwardCompatibleMode( + DP_BlendMode(DP_layer_props_blend_mode(lp))); + + if(isGroup) { + DP_transient_layer_list_merge_at(tll, lp, i); + DP_transient_layer_props_list_merge_at(tlpl, i); + } + + if(isModeIncompatible) { + DP_TransientLayerProps *tlp = + DP_transient_layer_props_list_transient_at_noinc(tlpl, i); + DP_transient_layer_props_blend_mode_set(tlp, DP_BLEND_MODE_NORMAL); + } + } + + // Other stuff like the timeline or document metadata will just get dropped. + return drawdance::CanvasState::noinc( + DP_transient_canvas_state_persist(tcs)); } CanvasState::CanvasState(DP_CanvasState *cs) - : m_data{cs} + : m_data{cs} { } void CanvasState::pushMessage(void *user, DP_Message *msg) { - static_cast(user)->append(drawdance::Message::noinc(msg)); + static_cast(user)->append(net::Message::noinc(msg)); } void CanvasState::addLayerVisibleInFrame(void *user, int layerId, bool visible) { - if(visible) { - QSet *layersVisibleInFrame = static_cast *>(user); - layersVisibleInFrame->insert(layerId); - } + if(visible) { + QSet *layersVisibleInFrame = static_cast *>(user); + layersVisibleInFrame->insert(layerId); + } } bool CanvasState::shouldCancelFloodFill(void *user) { - return *static_cast(user); + return *static_cast(user); } } diff --git a/src/libclient/drawdance/canvasstate.h b/src/libclient/drawdance/canvasstate.h index 682fac1238..39ab8d0788 100644 --- a/src/libclient/drawdance/canvasstate.h +++ b/src/libclient/drawdance/canvasstate.h @@ -1,20 +1,17 @@ // SPDX-License-Identifier: GPL-3.0-or-later - #ifndef DRAWDANCE_CANVASSTATE_H #define DRAWDANCE_CANVASSTATE_H - extern "C" { #include #include } - #include "libclient/drawdance/annotationlist.h" #include "libclient/drawdance/documentmetadata.h" #include "libclient/drawdance/layercontent.h" #include "libclient/drawdance/layerlist.h" -#include "libclient/drawdance/message.h" #include "libclient/drawdance/tile.h" #include "libclient/drawdance/timeline.h" +#include "libclient/net/message.h" #include #include #include @@ -28,79 +25,82 @@ namespace drawdance { class CanvasState final { public: - static CanvasState null(); - static CanvasState inc(DP_CanvasState *cs); - static CanvasState noinc(DP_CanvasState *cs); + static CanvasState null(); + static CanvasState inc(DP_CanvasState *cs); + static CanvasState noinc(DP_CanvasState *cs); - static CanvasState load(const QString &path, DP_LoadResult *outResult = nullptr); + static CanvasState + load(const QString &path, DP_LoadResult *outResult = nullptr); - CanvasState(); - CanvasState(const CanvasState &other); - CanvasState(CanvasState &&other); + CanvasState(); + CanvasState(const CanvasState &other); + CanvasState(CanvasState &&other); - CanvasState &operator=(const CanvasState &other); - CanvasState &operator=(CanvasState &&other); + CanvasState &operator=(const CanvasState &other); + CanvasState &operator=(CanvasState &&other); - ~CanvasState(); + ~CanvasState(); - DP_CanvasState *get() const; + DP_CanvasState *get() const; - bool isNull() const; + bool isNull() const; - int width() const; - int height() const; - QSize size() const; + int width() const; + int height() const; + QSize size() const; - int offsetX() const; - int offsetY() const; - QPoint offset() const; + int offsetX() const; + int offsetY() const; + QPoint offset() const; - Tile backgroundTile() const; - DocumentMetadata documentMetadata() const; - LayerList layers() const; - AnnotationList annotations() const; - Timeline timeline() const; + Tile backgroundTile() const; + DocumentMetadata documentMetadata() const; + LayerList layers() const; + AnnotationList annotations() const; + Timeline timeline() const; - int frameCount() const; - int framerate() const; + int frameCount() const; + int framerate() const; - bool sameFrame(int frameIndexA, int frameIndexB) const; + bool sameFrame(int frameIndexA, int frameIndexB) const; - QSet getLayersVisibleInTrackFrame(int trackId, int frameIndex) const; + QSet getLayersVisibleInTrackFrame(int trackId, int frameIndex) const; - QImage toFlatImage( - bool includeBackground = true, bool includeSublayers = true, - const QRect *rect = nullptr, const DP_ViewModeFilter *vmf = nullptr) const; + QImage toFlatImage( + bool includeBackground = true, bool includeSublayers = true, + const QRect *rect = nullptr, + const DP_ViewModeFilter *vmf = nullptr) const; - QImage layerToFlatImage(int layerId, const QRect &rect) const; + QImage layerToFlatImage(int layerId, const QRect &rect) const; - void toResetImage(MessageList &msgs, uint8_t contextId) const; + void toResetImage(net::MessageList &msgs, uint8_t contextId) const; - drawdance::Message makeLayerOrder( - uint8_t contextId, int sourceId, int targetId, bool below) const; + net::Message makeLayerOrder( + uint8_t contextId, int sourceId, int targetId, bool below) const; - drawdance::Message makeLayerTreeMove( - uint8_t contextId, int sourceId, int targetId, bool intoGroup, bool below) const; + net::Message makeLayerTreeMove( + uint8_t contextId, int sourceId, int targetId, bool intoGroup, + bool below) const; - LayerContent searchLayerContent(int layerId) const; + LayerContent searchLayerContent(int layerId) const; - DP_FloodFillResult floodFill( - int x, int y, const QColor &fillColor, double tolerance, int layerId, - int sizeLimit, int gap, int expand, int featherRadius, - const QAtomicInt &cancel, QImage &outImg, int &outX, int &outY) const; + DP_FloodFillResult floodFill( + int x, int y, const QColor &fillColor, double tolerance, int layerId, + int sizeLimit, int gap, int expand, int featherRadius, + const QAtomicInt &cancel, QImage &outImg, int &outX, int &outY) const; - drawdance::CanvasState makeBackwardCompatible() const; + drawdance::CanvasState makeBackwardCompatible() const; private: - explicit CanvasState(DP_CanvasState *cs); + explicit CanvasState(DP_CanvasState *cs); - static void pushMessage(void *user, DP_Message *msg); + static void pushMessage(void *user, DP_Message *msg); - static void addLayerVisibleInFrame(void *user, int layerId, bool visible); + static void addLayerVisibleInFrame(void *user, int layerId, bool visible); - static bool shouldCancelFloodFill(void *user); + static bool shouldCancelFloodFill(void *user); - DP_CanvasState *m_data; + DP_CanvasState *m_data; }; } diff --git a/src/libclient/drawdance/message.cpp b/src/libclient/drawdance/message.cpp deleted file mode 100644 index 6a9a4dd599..0000000000 --- a/src/libclient/drawdance/message.cpp +++ /dev/null @@ -1,801 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -extern "C" { -#include -#include -} - -#include "libclient/drawdance/message.h" - -#include "libclient/canvas/blendmodes.h" -#include "libclient/drawdance/global.h" -#include "libclient/drawdance/tile.h" -#include "libshared/util/qtcompat.h" - -#include -#include -#include -#include -#include - -namespace drawdance { - -Message Message::null() -{ - return Message{nullptr}; -} - -Message Message::inc(DP_Message *msg) -{ - return Message{DP_message_incref_nullable(msg)}; -} - -Message Message::noinc(DP_Message *msg) -{ - return Message{msg}; -} - -Message Message::deserialize(const unsigned char *buf, size_t bufsize) -{ - return Message::noinc(DP_message_deserialize(buf, bufsize)); -} - - -DP_Message **Message::asRawMessages(const drawdance::Message *msgs) -{ - // We want to do a moderately evil reinterpret cast of a drawdance::Message - // to its underlying pointer. Let's make sure that it's a valid thing to do. - // Make sure it's a standard layout class, because only for those it's legal - // to cast them to their first member. - static_assert(std::is_standard_layout::value, - "drawdance::Message is standard layout for reinterpretation to DP_Message"); - // And then ensure that there's only the pointer member. - static_assert(sizeof(drawdance::Message) == sizeof(DP_Message *), - "drawdance::Message has the same size as a DP_Message pointer"); - // Alright, that means this cast, despite looking terrifying, is legal. The - // const can be cast away safely too because the underlying pointer isn't. - return reinterpret_cast(const_cast(msgs)); -} - - -Message Message::makeAnnotationCreate(unsigned int contextId, uint16_t id, int32_t x, int32_t y, uint16_t w, uint16_t h) -{ - return Message{DP_msg_annotation_create_new(contextId, id, x, y, w, h)}; -} - -Message Message::makeAnnotationDelete(uint8_t contextId, uint16_t id) -{ - return Message{DP_msg_annotation_delete_new(contextId, id)}; -} - -Message Message::makeAnnotationEdit(uint8_t contextId, uint16_t id, uint32_t bg, uint8_t flags, uint8_t border, const QString &text) -{ - QByteArray bytes = text.toUtf8(); - return Message{DP_msg_annotation_edit_new( - contextId, id, bg, flags, border, bytes.constData(), bytes.length())}; -} - -Message Message::makeAnnotationReshape(uint8_t contextId, uint16_t id, int32_t x, int32_t y, uint16_t w, uint16_t h) -{ - return Message{DP_msg_annotation_reshape_new(contextId, id, x, y, w, h)}; -} - -Message Message::makeCanvasBackground(uint8_t contextId, const QColor &color) -{ - uint32_t c = qToBigEndian(color.rgba()); - return Message{DP_msg_canvas_background_new(contextId, setUchars, sizeof(c), &c)}; -} - -Message Message::makeCanvasResize(uint8_t contextId, int32_t top, int32_t right, int32_t bottom, int32_t left) -{ - return Message{DP_msg_canvas_resize_new(contextId, top, right, bottom, left)}; -} - -Message Message::makeChat(uint8_t contextId, uint8_t tflags, uint8_t oflags, const QString &message) -{ - QByteArray bytes = message.toUtf8(); - return Message{DP_msg_chat_new(contextId, tflags, oflags, bytes.constData(), bytes.length())}; -} - -Message Message::makeDefaultLayer(uint8_t contextId, uint16_t id) -{ - return Message{DP_msg_default_layer_new(contextId, id)}; -} - -Message Message::makeDisconnect(uint8_t contextId, uint8_t reason, const QString &message) -{ - QByteArray bytes = message.toUtf8(); - return Message{DP_msg_disconnect_new(contextId, reason, bytes.constData(), bytes.length())}; -} - -Message Message::makeFeatureAccessLevels(uint8_t contextId, int featureCount, const uint8_t *features) -{ - return Message{DP_msg_feature_access_levels_new(contextId, setUint8s, featureCount, const_cast(features))}; -} - -Message Message::makeFillRect(uint8_t contextId, uint16_t layer, uint8_t mode, uint32_t x, uint32_t y, uint32_t w, uint32_t h, const QColor &color) -{ - return Message{DP_msg_fill_rect_new(contextId, layer, mode, x, y, w, h, color.rgba())}; -} - -Message Message::makeInternalCatchup(uint8_t contextId, int progress) -{ - return Message{DP_msg_internal_catchup_new(contextId, progress)}; -} - -Message Message::makeInternalCleanup(uint8_t contextId) -{ - return Message{DP_msg_internal_cleanup_new(contextId)}; -} - -Message Message::makeInternalReset(uint8_t contextId) -{ - return Message{DP_msg_internal_reset_new(contextId)}; -} - -Message Message::makeInternalSnapshot(uint8_t contextId) -{ - return Message{DP_msg_internal_snapshot_new(contextId)}; -} - -Message Message::makeKeyFrameLayerAttributes(uint8_t contextId, uint16_t trackId, uint16_t frameIndex, const QVector &layers) -{ - return Message{DP_msg_key_frame_layer_attributes_new(contextId, trackId, frameIndex, setUint16s, layers.size(), const_cast(layers.constData()))}; -} - -Message Message::makeKeyFrameSet(uint8_t contextId, uint16_t trackId, uint16_t frameIndex, uint16_t sourceId, uint16_t sourceIndex, uint8_t source) -{ - return Message{DP_msg_key_frame_set_new(contextId, trackId, frameIndex, sourceId, sourceIndex, source)}; -} - -Message Message::makeKeyFrameRetitle(uint8_t contextId, uint16_t trackId, uint16_t frameIndex, const QString &title) -{ - QByteArray bytes = title.toUtf8(); - return Message{DP_msg_key_frame_retitle_new(contextId, trackId, frameIndex, bytes.constData(), bytes.length())}; -} - -Message Message::makeKeyFrameDelete(uint8_t contextId, uint16_t trackId, uint16_t frameIndex, uint16_t moveTrackId, uint16_t moveFrameIndex) -{ - return Message{DP_msg_key_frame_delete_new(contextId, trackId, frameIndex, moveTrackId, moveFrameIndex)}; -} - -Message Message::makeLaserTrail(uint8_t contextId, uint32_t color, uint8_t persistence) -{ - return Message{DP_msg_laser_trail_new(contextId, color, persistence)}; -} - -Message Message::makeLayerAttributes(uint8_t contextId, uint16_t id, uint8_t sublayer, uint8_t flags, uint8_t opacity, uint8_t blend) -{ - return Message{DP_msg_layer_attributes_new(contextId, id, sublayer, flags, opacity, blend)}; -} - -Message Message::makeLayerAcl(uint8_t contextId, uint16_t id, uint8_t flags, const QVector &exclusive) -{ - return Message{DP_msg_layer_acl_new( - contextId, id, flags, setUint8s, exclusive.count(), const_cast(exclusive.constData()))}; -} - -Message Message::makeLayerCreate(uint8_t contextId, uint16_t id, uint16_t source, uint32_t fill, uint8_t flags, const QString &name) -{ - QByteArray bytes = name.toUtf8(); - return Message{DP_msg_layer_create_new( - contextId, id, source, fill, flags, bytes.constData(), bytes.length())}; -} - -Message Message::makeLayerTreeCreate(uint8_t contextId, uint16_t id, uint16_t source, uint16_t target, uint32_t fill, uint8_t flags, const QString &name) -{ - QByteArray bytes = name.toUtf8(); - return Message{DP_msg_layer_tree_create_new( - contextId, id, source, target, fill, flags, bytes.constData(), bytes.length())}; -} - -Message Message::makeLayerDelete(uint8_t contextId, uint16_t id, bool merge) -{ - return Message{DP_msg_layer_delete_new(contextId, id, merge)}; -} - -Message Message::makeLayerTreeDelete(uint8_t contextId, uint16_t id, uint16_t mergeTo) -{ - return Message{DP_msg_layer_tree_delete_new(contextId, id, mergeTo)}; -} - -Message Message::makeLayerTreeMove(uint8_t contextId, uint16_t layer, uint16_t parent, uint16_t sibling) -{ - return Message{DP_msg_layer_tree_move_new(contextId, layer, parent, sibling)}; -} - -Message Message::makeLayerRetitle(uint8_t contextId, uint16_t id, const QString &title) -{ - QByteArray bytes = title.toUtf8(); - return Message{DP_msg_layer_retitle_new(contextId, id, bytes.constData(), bytes.length())}; -} - -Message Message::makeMovePointer(uint8_t contextId, int32_t x, int32_t y) -{ - return Message{DP_msg_move_pointer_new(contextId, x, y)}; -} - -Message Message::makeMoveRegion(uint8_t contextId, uint16_t layer, int32_t bx, int32_t by, int32_t bw, int32_t bh, int32_t x1, int32_t y1, int32_t x2, int32_t y2, int32_t x3, int32_t y3, int32_t x4, int32_t y4, const QImage &mask) -{ - QByteArray compressed = mask.isNull() ? QByteArray{} : qCompress(mask.constBits(), mask.sizeInBytes()); - if(compressed.size() <= DP_MESSAGE_MAX_PAYLOAD_LENGTH - DP_MSG_MOVE_REGION_STATIC_LENGTH) { - return Message{DP_msg_move_region_new(contextId, layer, bx, by, bw, bh, x1, y1, x2, y2, x3, y3, x4, y4, &Message::setUchars, compressed.size(), compressed.data())}; - } else { - return Message::null(); - } -} - -Message Message::makeMoveRect(uint8_t contextId, uint16_t layer, uint16_t source, int32_t sx, int32_t sy, int32_t tx, int32_t ty, int32_t w, int32_t h, const QImage &mask) -{ - QByteArray compressed = mask.isNull() ? QByteArray{} : compressAlphaMask(mask); - if(compressed.size() <= DP_MESSAGE_MAX_PAYLOAD_LENGTH - DP_MSG_MOVE_RECT_STATIC_LENGTH) { - return Message{DP_msg_move_rect_new(contextId, layer, source, sx, sy, tx, ty, w, h, &Message::setUchars, compressed.size(), compressed.data())}; - } else { - return Message::null(); - } -} - -Message Message::makeTransformRegion(uint8_t contextId, uint16_t layer, uint16_t source, int32_t bx, int32_t by, int32_t bw, int32_t bh, int32_t x1, int32_t y1, int32_t x2, int32_t y2, int32_t x3, int32_t y3, int32_t x4, int32_t y4, uint8_t mode, const QImage &mask) -{ - QByteArray compressed = mask.isNull() ? QByteArray{} : compressAlphaMask(mask); - if(compressed.size() <= DP_MESSAGE_MAX_PAYLOAD_LENGTH - DP_MSG_TRANSFORM_REGION_STATIC_LENGTH) { - return Message{DP_msg_transform_region_new(contextId, layer, source, bx, by, bw, bh, x1, y1, x2, y2, x3, y3, x4, y4, mode, &Message::setUchars, compressed.size(), compressed.data())}; - } else { - return Message::null(); - } -} - -Message Message::makePing(uint8_t contextId, bool isPong) -{ - return Message{DP_msg_ping_new(contextId, isPong)}; -} - -Message Message::makePrivateChat(uint8_t contextId, uint8_t target, uint8_t oflags, const QString &message) -{ - QByteArray bytes = message.toUtf8(); - return Message{DP_msg_private_chat_new(contextId, target, oflags, bytes.constData(), bytes.length())}; -} - -Message Message::makePutImage(uint8_t contextId, uint16_t layer, uint8_t mode, uint32_t x, uint32_t y, uint32_t w, uint32_t h, const QByteArray &compressedImage) -{ - return Message{DP_msg_put_image_new( - contextId, layer, mode, x, y, w, h, setUchars, compressedImage.size(), const_cast(compressedImage.data()))}; -} - -Message Message::makeServerCommand(uint8_t contextId, const QJsonDocument &msg) -{ - QByteArray msgBytes = msg.toJson(QJsonDocument::Compact); - if(msgBytes.length() <= DP_MESSAGE_MAX_PAYLOAD_LENGTH - DP_MSG_SERVER_COMMAND_STATIC_LENGTH) { - return Message{DP_msg_server_command_new(contextId, msgBytes.constData(), msgBytes.length())}; - } else { - qWarning("ServerCommand too long (%lld bytes)", compat::cast(msgBytes.length())); - return drawdance::Message::null(); - } -} - -Message Message::makeSessionOwner(uint8_t contextId, const QVector &users) -{ - return Message{DP_msg_session_owner_new( - contextId, setUint8s, users.count(), const_cast(users.constData()))}; -} - -Message Message::makeSetMetadataInt(uint8_t contextId, uint8_t field, int32_t value) -{ - return Message{DP_msg_set_metadata_int_new(contextId, field, value)}; -} - -Message Message::makeTrackCreate(uint8_t contextId, uint16_t id, uint16_t insertId, uint16_t sourceId, const QString &title) -{ - QByteArray bytes = title.toUtf8(); - return Message{DP_msg_track_create_new(contextId, id, insertId, sourceId, bytes.constData(), bytes.length())}; -} - -Message Message::makeTrackDelete(uint8_t contextId, uint16_t id) -{ - return Message{DP_msg_track_delete_new(contextId, id)}; -} - -Message Message::makeTrackOrder(uint8_t contextId, const QVector &tracks) -{ - return Message{DP_msg_track_order_new(contextId, setUint16s, tracks.size(), const_cast(tracks.constData()))}; -} - -Message Message::makeTrackRetitle(uint8_t contextId, uint16_t id, const QString &title) -{ - QByteArray bytes = title.toUtf8(); - return Message{DP_msg_track_retitle_new(contextId, id, bytes.constData(), bytes.length())}; -} - -Message Message::makeTrustedUsers(uint8_t contextId, const QVector &users) -{ - return Message{DP_msg_trusted_users_new( - contextId, setUint8s, users.count(), const_cast(users.constData()))}; -} - -Message Message::makeUndo(uint8_t contextId, uint8_t overrideUser, bool redo) -{ - return Message{DP_msg_undo_new(contextId, overrideUser, redo)}; -} - -Message Message::makeUndoDepth(uint8_t contextId, uint8_t depth) -{ - return Message{DP_msg_undo_depth_new(contextId, depth)}; -} - -Message Message::makeUndoPoint(uint8_t contextId) -{ - return Message{DP_msg_undo_point_new(contextId)}; -} - -Message Message::makeUserAcl(uint8_t contextId, const QVector &users) -{ - return Message{DP_msg_user_acl_new( - contextId, setUint8s, users.count(), const_cast(users.constData()))}; -} - -Message Message::makeUserInfo(uint8_t contextId, uint8_t recipient, const QJsonDocument &msg) -{ - QByteArray msgBytes = msg.toJson(QJsonDocument::Compact); - return Message{DP_msg_data_new( - contextId, DP_MSG_DATA_TYPE_USER_INFO, recipient, setUchars, - msgBytes.length(), const_cast(msgBytes.constData()))}; -} - - -void Message::makePutImages(MessageList &msgs, uint8_t contextId, uint16_t layer, uint8_t mode, int x, int y, const QImage &image) -{ - // If the image is totally outside of the canvas, there's nothing to put. - if(x >= -image.width() && y >= -image.height()) { - QImage converted = image.convertToFormat(QImage::Format_ARGB32_Premultiplied); - if(x < 0 || y < 0) { - // Crop image, since the protocol doesn't do negative coordinates. - int xoffset = x < 0 ? -x : 0; - int yoffset = y < 0 ? -y : 0; - QImage cropped = converted.copy(xoffset, yoffset, image.width() - xoffset, image.height() - yoffset); - makePutImagesRecursive(msgs, contextId, layer, mode, x + xoffset, y + yoffset, cropped, cropped.rect()); - } else { - makePutImagesRecursive(msgs, contextId, layer, mode, x, y, converted, converted.rect()); - } - } -} - - -Message Message::makeLocalChangeLayerVisibility(int layerId, bool hidden) -{ - return Message{DP_local_state_msg_layer_visibility_new(layerId, hidden)}; -} - -Message Message::makeLocalChangeBackgroundColor(const QColor &color) -{ - DrawContext drawContext = DrawContextPool::acquire(); - Tile tile = Tile::fromColor(color); - return Message{ - DP_local_state_msg_background_tile_new(drawContext.get(), tile.get())}; -} - -Message Message::makeLocalChangeBackgroundClear() -{ - return Message{DP_local_state_msg_background_tile_new(nullptr, nullptr)}; -} - - -Message Message::makeLocalChangeViewMode(DP_ViewMode viewMode) -{ - return Message{DP_local_state_msg_view_mode_new(viewMode)}; -} - -Message Message::makeLocalChangeActiveLayer(int layerId) -{ - return Message{DP_local_state_msg_active_layer_new(layerId)}; -} - -Message Message::makeLocalChangeActiveFrame(int frameIndex) -{ - return Message{DP_local_state_msg_active_frame_new(frameIndex)}; -} - -Message Message::makeLocalChangeOnionSkins(const DP_OnionSkins *oss) -{ - return Message{DP_local_state_msg_onion_skins_new(oss)}; -} - -Message Message::makeLocalChangeTrackVisibility(int trackId, bool hidden) -{ - return Message{DP_local_state_msg_track_visibility_new(trackId, hidden)}; -} -Message Message::makeLocalChangeTrackOnionSkin(int trackId, bool onionSkin) -{ - return Message{DP_local_state_msg_track_onion_skin_new(trackId, onionSkin)}; -} - - -Message::Message() - : Message(nullptr) -{ -} - -Message::Message(const Message &other) - : Message{DP_message_incref_nullable(other.m_data)} -{ -} - -Message::Message(Message &&other) - : Message{other.m_data} -{ - other.m_data = nullptr; -} - -Message &Message::operator=(const Message &other) -{ - DP_message_decref_nullable(m_data); - m_data = DP_message_incref_nullable(other.m_data); - return *this; -} - -Message &Message::operator=(Message &&other) -{ - DP_message_decref_nullable(m_data); - m_data = other.m_data; - other.m_data = nullptr; - return *this; -} - -Message::~Message() -{ - DP_message_decref_nullable(m_data); -} - -DP_Message *Message::get() const -{ - return m_data; -} - -bool Message::isNull() const -{ - return !m_data; -} - -DP_MessageType Message::type() const -{ - return DP_message_type(m_data); -} - -QString Message::typeName() const -{ - return QString::fromUtf8(DP_message_type_name(type())); -} - -unsigned int Message::contextId() const -{ - return DP_message_context_id(m_data); -} - -size_t Message::length() const -{ - return DP_message_length(m_data); -} - -DP_MsgServerCommand *Message::toServerCommand() const -{ - return DP_msg_server_command_cast(m_data); -} - -DP_MsgData *Message::toData() const -{ - return DP_msg_data_cast(m_data); -} - -bool Message::serialize(QByteArray &buffer) const -{ - return DP_message_serialize(m_data, true, getDeserializeBuffer, &buffer) != 0; -} - -Message Message::makeBackwardCompatible() const -{ - switch(type()) { - case DP_MSG_SERVER_COMMAND: - case DP_MSG_DISCONNECT: - case DP_MSG_PING: - case DP_MSG_INTERNAL: - case DP_MSG_JOIN: - case DP_MSG_LEAVE: - case DP_MSG_SESSION_OWNER: - case DP_MSG_CHAT: - case DP_MSG_TRUSTED_USERS: - case DP_MSG_SOFT_RESET: - case DP_MSG_PRIVATE_CHAT: - case DP_MSG_INTERVAL: - case DP_MSG_LASER_TRAIL: - case DP_MSG_MOVE_POINTER: - case DP_MSG_MARKER: - case DP_MSG_LAYER_ACL: - case DP_MSG_DEFAULT_LAYER: - case DP_MSG_FILTERED: - case DP_MSG_EXTENSION: - case DP_MSG_UNDO_POINT: - case DP_MSG_CANVAS_RESIZE: - case DP_MSG_LAYER_CREATE: - case DP_MSG_LAYER_RETITLE: - case DP_MSG_LAYER_ORDER: - case DP_MSG_LAYER_DELETE: - case DP_MSG_PEN_UP: - case DP_MSG_ANNOTATION_CREATE: - case DP_MSG_ANNOTATION_RESHAPE: - case DP_MSG_ANNOTATION_EDIT: - case DP_MSG_ANNOTATION_DELETE: - case DP_MSG_MOVE_REGION: - case DP_MSG_PUT_TILE: - case DP_MSG_CANVAS_BACKGROUND: - case DP_MSG_UNDO: - return *this; - case DP_MSG_USER_ACL: { - DP_MsgUserAcl *mua = DP_msg_user_acl_cast(m_data); - int count; - const uint8_t *users = DP_msg_user_acl_users(mua, &count); - QVector compatibleUsers; - compatibleUsers.reserve(count); - for(int i = 0; i < count; ++i) { - uint8_t user = users[i]; - if(user != 0) { // Reset locks aren't in Drawpile 2.1. - compatibleUsers.append(user); - } - } - int compatibleCount = compatibleUsers.size(); - if(compatibleCount == count) { - return *this; - } else { - qDebug("Making %s message compatible", qUtf8Printable(typeName())); - return noinc(DP_msg_feature_access_levels_new( - contextId(), setUint8s, compatibleCount, compatibleUsers.data())); - } - } - case DP_MSG_FEATURE_ACCESS_LEVELS: { - DP_MsgFeatureAccessLevels *mfal = DP_msg_feature_access_levels_cast(m_data); - int count; - const uint8_t *tiers = - DP_msg_feature_access_levels_feature_tiers(mfal, &count); - if(count == 9) { - return *this; - } else { - qDebug("Making %s message compatible", qUtf8Printable(typeName())); - uint8_t compatibleTiers[9]; - for(int i = 0; i < 9; ++i) { - compatibleTiers[i] = i < count ? tiers[i] : 0; - } - return noinc(DP_msg_feature_access_levels_new( - contextId(), setUint8s, 9, compatibleTiers)); - } - } - case DP_MSG_LAYER_ATTRIBUTES: { - DP_MsgLayerAttributes *mla = DP_msg_layer_attributes_cast(m_data); - bool compatible = canvas::blendmode::isBackwardCompatibleMode( - DP_BlendMode(DP_msg_layer_attributes_blend(mla))); - if(compatible) { - return *this; - } else { - qDebug("Making %s message compatible", qUtf8Printable(typeName())); - return noinc(DP_msg_layer_attributes_new( - contextId(), DP_msg_layer_attributes_id(mla), - DP_msg_layer_attributes_sublayer(mla), - DP_msg_layer_attributes_flags(mla), - DP_msg_layer_attributes_opacity(mla), DP_BLEND_MODE_NORMAL)); - } - } - case DP_MSG_PUT_IMAGE: { - DP_MsgPutImage *mpi = DP_msg_put_image_cast(m_data); - bool compatible = canvas::blendmode::isBackwardCompatibleMode( - DP_BlendMode(DP_msg_put_image_mode(mpi))); - if(compatible) { - return *this; - } else { - qDebug("Making %s message compatible", qUtf8Printable(typeName())); - size_t size; - const unsigned char *image = DP_msg_put_image_image(mpi, &size); - return noinc(DP_msg_put_image_new( - contextId(), DP_msg_put_image_layer(mpi), - DP_BLEND_MODE_NORMAL, DP_msg_put_image_x(mpi), - DP_msg_put_image_y(mpi), DP_msg_put_image_w(mpi), - DP_msg_put_image_h(mpi), setUchars, size, - const_cast(image))); - } - } - case DP_MSG_FILL_RECT: { - DP_MsgFillRect *mfr = DP_msg_fill_rect_cast(m_data); - bool compatible = canvas::blendmode::isBackwardCompatibleMode( - DP_BlendMode(DP_msg_fill_rect_mode(mfr))); - if(compatible) { - return *this; - } else { - qDebug("Making %s message compatible", qUtf8Printable(typeName())); - return noinc(DP_msg_fill_rect_new( - contextId(), DP_msg_fill_rect_layer(mfr), DP_BLEND_MODE_NORMAL, - DP_msg_fill_rect_x(mfr), DP_msg_fill_rect_y(mfr), - DP_msg_fill_rect_w(mfr), DP_msg_fill_rect_h(mfr), - DP_msg_fill_rect_color(mfr))); - } - } - case DP_MSG_DRAW_DABS_CLASSIC: { - DP_MsgDrawDabsClassic *mddc = DP_msg_draw_dabs_classic_cast(m_data); - bool compatible = canvas::blendmode::isBackwardCompatibleMode( - DP_BlendMode(DP_msg_draw_dabs_classic_mode(mddc))); - if(compatible) { - return *this; - } else { - qDebug("Making %s message compatible", qUtf8Printable(typeName())); - int count; - const DP_ClassicDab *cds = - DP_msg_draw_dabs_classic_dabs(mddc, &count); - return noinc(DP_msg_draw_dabs_classic_new( - contextId(), DP_msg_draw_dabs_classic_layer(mddc), - DP_msg_draw_dabs_classic_x(mddc), - DP_msg_draw_dabs_classic_y(mddc), - DP_msg_draw_dabs_classic_color(mddc), DP_BLEND_MODE_NORMAL, - setClassicDabs, count, const_cast(cds))); - } - } - case DP_MSG_DRAW_DABS_PIXEL: - case DP_MSG_DRAW_DABS_PIXEL_SQUARE: { - DP_MsgDrawDabsPixel *mddp = - static_cast(DP_message_internal(m_data)); - bool compatible = canvas::blendmode::isBackwardCompatibleMode( - DP_BlendMode(DP_msg_draw_dabs_pixel_mode(mddp))); - if(compatible) { - return *this; - } else { - qDebug("Making %s message compatible", qUtf8Printable(typeName())); - int count; - const DP_PixelDab *pds = DP_msg_draw_dabs_pixel_dabs(mddp, &count); - return noinc((type() == DP_MSG_DRAW_DABS_PIXEL - ? DP_msg_draw_dabs_pixel_new - : DP_msg_draw_dabs_pixel_square_new)( - contextId(), DP_msg_draw_dabs_pixel_layer(mddp), - DP_msg_draw_dabs_pixel_x(mddp), DP_msg_draw_dabs_pixel_y(mddp), - DP_msg_draw_dabs_pixel_color(mddp), DP_BLEND_MODE_NORMAL, - setPixelDabs, count, const_cast(pds))); - } - } - case DP_MSG_LAYER_TREE_CREATE: { - DP_MsgLayerTreeCreate *mltc = DP_msg_layer_tree_create_cast(m_data); - uint8_t flags = DP_msg_layer_tree_create_flags(mltc); - uint16_t sourceId = DP_msg_layer_tree_create_source(mltc); - uint16_t targetId = DP_msg_layer_tree_create_target(mltc); - bool involvesGroups = (flags & DP_MSG_LAYER_TREE_CREATE_FLAGS_GROUP) - || (flags & DP_MSG_LAYER_TREE_CREATE_FLAGS_INTO); - bool sourceAndTargetDiffer = - sourceId != 0 && targetId != 0 && sourceId != targetId; - if(involvesGroups || sourceAndTargetDiffer) { - return null(); - } else { - qDebug("Making %s message compatible", qUtf8Printable(typeName())); - uint8_t compatFlags = - (sourceId == 0 ? 0 : DP_MSG_LAYER_CREATE_FLAGS_COPY) | - (targetId == 0 ? 0 : DP_MSG_LAYER_CREATE_FLAGS_INSERT); - uint16_t compatSourceId = sourceId == 0 ? targetId : sourceId; - size_t titleLength; - const char *title = - DP_msg_layer_tree_create_title(mltc, &titleLength); - return noinc(DP_msg_layer_create_new( - contextId(), DP_msg_layer_tree_create_id(mltc), compatSourceId, - DP_msg_layer_tree_create_fill(mltc), compatFlags, title, - titleLength)); - } - } - default: - return null(); - } -} - -void Message::setIndirectCompatFlag() -{ - DP_message_compat_flag_indirect_set(m_data); -} - -Message::Message(DP_Message *msg) - : m_data{msg} -{ -} - -QByteArray Message::compressAlphaMask(const QImage &mask) -{ - Q_ASSERT(mask.format() == QImage::Format_ARGB32_Premultiplied); - int width = mask.width(); - int height = mask.height(); - QByteArray alphaMask; - alphaMask.reserve(width * height); - for(int y = 0; y < height; ++y) { - const uchar *scanLine = mask.scanLine(y); - for(int x = 0; x < width; ++x) { - alphaMask.append(scanLine[x * 4 + 3]); - } - } - return qCompress(alphaMask); -} - -void Message::makePutImagesRecursive(MessageList &msgs, uint8_t contextId, uint16_t layer, uint8_t mode, int x, int y, const QImage &image, const QRect &bounds, int estimatedSize) -{ - int w = bounds.width(); - int h = bounds.height(); - if (w > 0 && h > 0) { - int maxSize = 0xffff - 19; - int compressedSize; - // If our estimated size looks good, try compressing. Otherwise assume - // that the image is too big to fit into a message and split it up. - if(estimatedSize < maxSize) { - QImage subImage = bounds == image.rect() ? image : image.copy(bounds); - QByteArray compressed = qCompress(subImage.constBits(), subImage.sizeInBytes()); - compressedSize = compressed.size(); - if(compressedSize <= maxSize) { - msgs.append(makePutImage(contextId, layer, mode, x, y, w, h, compressed)); - return; - } - } else { - compressedSize = estimatedSize; - } - // (Probably) too big to fit in a message, slice in half along the longest axis. - int estimatedSliceSize = compressedSize / 2; - if (w > h) { - int sx1 = w / 2; - int sx2 = w - sx1; - makePutImagesRecursive(msgs, contextId, layer, mode, x, y, image, bounds.adjusted(0, 0, -sx1, 0), estimatedSliceSize); - makePutImagesRecursive(msgs, contextId, layer, mode, x + sx2, y, image, bounds.adjusted(sx2, 0, 0, 0), estimatedSliceSize); - } else { - int sy1 = h / 2; - int sy2 = h - sy1; - makePutImagesRecursive(msgs, contextId, layer, mode, x, y, image, bounds.adjusted(0, 0, 0, -sy1), estimatedSliceSize); - makePutImagesRecursive(msgs, contextId, layer, mode, x, y + sy2, image, bounds.adjusted(0, sy2, 0, 0), estimatedSliceSize); - } - } -} - -unsigned char *Message::getDeserializeBuffer(void *user, size_t size) -{ - QByteArray *buffer = static_cast(user); - buffer->resize(compat::castSize(size)); - return reinterpret_cast(buffer->data()); -} - -void Message::setUchars(size_t size, unsigned char *out, void *user) -{ - if(size > 0) { - memcpy(out, user, size); - } -} - -void Message::setUint8s(int count, uint8_t *out, void *user) -{ - if(count > 0) { - memcpy(out, user, sizeof(uint8_t) * count); - } -} - -void Message::setUint16s(int count, uint16_t *out, void *user) -{ - if(count > 0) { - memcpy(out, user, sizeof(uint16_t) * count); - } -} - -void Message::setClassicDabs(int count, DP_ClassicDab *out, void *user) -{ - const DP_ClassicDab *cds = static_cast(user); - for(int i = 0; i < count; ++i) { - const DP_ClassicDab *cd = DP_classic_dab_at(cds, i); - DP_classic_dab_init( - out, i, DP_classic_dab_x(cd), DP_classic_dab_y(cd), - DP_classic_dab_size(cd), DP_classic_dab_hardness(cd), - DP_classic_dab_opacity(cd)); - } -} - -void Message::setPixelDabs(int count, DP_PixelDab *out, void *user) -{ - const DP_PixelDab *pds = static_cast(user); - for(int i = 0; i < count; ++i) { - const DP_PixelDab *pd = DP_pixel_dab_at(pds, i); - DP_pixel_dab_init( - out, i, DP_pixel_dab_x(pd), DP_pixel_dab_y(pd), - DP_pixel_dab_size(pd), DP_pixel_dab_opacity(pd)); - } -} - -} diff --git a/src/libclient/drawdance/message.h b/src/libclient/drawdance/message.h deleted file mode 100644 index c2108bf8de..0000000000 --- a/src/libclient/drawdance/message.h +++ /dev/null @@ -1,163 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -#ifndef DRAWDANCE_MESSAGE_H -#define DRAWDANCE_MESSAGE_H - -extern "C" { -#include -#include -} - -#include -#include - -struct DP_OnionSkins; -struct DP_Message; - -class QByteArray; -class QColor; -class QImage; -class QJsonDocument; -class QString; - -namespace drawdance { - -using MessageList = QVector; - -class Message final { -public: - static Message null(); - static Message inc(DP_Message *cs); - static Message noinc(DP_Message *cs); - static Message deserialize(const unsigned char *buf, size_t bufsize); - - static DP_Message **asRawMessages(const drawdance::Message *msgs); - - static Message makeAnnotationCreate(unsigned int contextId, uint16_t id, int32_t x, int32_t y, uint16_t w, uint16_t h); - static Message makeAnnotationDelete(uint8_t contextId, uint16_t id); - static Message makeAnnotationEdit(uint8_t contextId, uint16_t id, uint32_t bg, uint8_t flags, uint8_t border, const QString &text); - static Message makeAnnotationReshape(uint8_t contextId, uint16_t id, int32_t x, int32_t y, uint16_t w, uint16_t h); - static Message makeCanvasBackground(uint8_t contextId, const QColor &color); - static Message makeCanvasResize(uint8_t contextId, int32_t top, int32_t right, int32_t bottom, int32_t left); - static Message makeChat(uint8_t contextId, uint8_t tflags, uint8_t oflags, const QString &message); - static Message makeDefaultLayer(uint8_t contextId, uint16_t id); - static Message makeDisconnect(uint8_t contextId, uint8_t reason, const QString &message); - static Message makeFeatureAccessLevels(uint8_t contextId, int featureCount, const uint8_t *features); - static Message makeFillRect(uint8_t contextId, uint16_t layer, uint8_t mode, uint32_t x, uint32_t y, uint32_t w, uint32_t h, const QColor &color); - static Message makeInternalCatchup(uint8_t contextId, int progress); - static Message makeInternalCleanup(uint8_t contextId); - static Message makeInternalReset(uint8_t contextId); - static Message makeInternalSnapshot(uint8_t contextId); - static Message makeKeyFrameSet(uint8_t contextId, uint16_t trackId, uint16_t frameIndex, uint16_t sourceId, uint16_t sourceIndex, uint8_t source); - static Message makeKeyFrameLayerAttributes(uint8_t contextId, uint16_t trackId, uint16_t frameIndex, const QVector &layers); - static Message makeKeyFrameRetitle(uint8_t contextId, uint16_t trackId, uint16_t frameIndex, const QString &title); - static Message makeKeyFrameDelete(uint8_t contextId, uint16_t trackId, uint16_t frameIndex, uint16_t moveTrackId, uint16_t moveFrameIndex); - static Message makeLaserTrail(uint8_t contextId, uint32_t color, uint8_t persistence); - static Message makeLayerAttributes(uint8_t contextId, uint16_t id, uint8_t sublayer, uint8_t flags, uint8_t opacity, uint8_t blend); - static Message makeLayerAcl(uint8_t contextId, uint16_t id, uint8_t flags, const QVector &exclusive); - static Message makeLayerCreate(uint8_t contextId, uint16_t id, uint16_t source, uint32_t fill, uint8_t flags, const QString &name); - static Message makeLayerTreeCreate(uint8_t contextId, uint16_t id, uint16_t source, uint16_t target, uint32_t fill, uint8_t flags, const QString &name); - static Message makeLayerDelete(uint8_t contextId, uint16_t id, bool merge); - static Message makeLayerTreeDelete(uint8_t contextId, uint16_t id, uint16_t mergeTo); - static Message makeLayerTreeMove(uint8_t contextId, uint16_t layer, uint16_t parent, uint16_t sibling); - static Message makeLayerRetitle(uint8_t contextId, uint16_t id, const QString &title); - static Message makeMovePointer(uint8_t contextId, int32_t x, int32_t y); - static Message makeMoveRegion(uint8_t contextId, uint16_t layer, int32_t bx, int32_t by, int32_t bw, int32_t bh, int32_t x1, int32_t y1, int32_t x2, int32_t y2, int32_t x3, int32_t y3, int32_t x4, int32_t y4, const QImage &mask); - static Message makeMoveRect(uint8_t contextId, uint16_t layer, uint16_t source, int32_t sx, int32_t sy, int32_t tx, int32_t ty, int32_t w, int32_t h, const QImage &mask); - static Message makeTransformRegion(uint8_t contextId, uint16_t layer, uint16_t source, int32_t bx, int32_t by, int32_t bw, int32_t bh, int32_t x1, int32_t y1, int32_t x2, int32_t y2, int32_t x3, int32_t y3, int32_t x4, int32_t y4, uint8_t mode, const QImage &mask); - static Message makePing(uint8_t contextId, bool isPong); - static Message makePrivateChat(uint8_t contextId, uint8_t target, uint8_t oflags, const QString &message); - static Message makePutImage(uint8_t contextId, uint16_t layer, uint8_t mode, uint32_t x, uint32_t y, uint32_t w, uint32_t h, const QByteArray &compressedImage); - static Message makeServerCommand(uint8_t contextId, const QJsonDocument &msg); - static Message makeSessionOwner(uint8_t contextId, const QVector &users); - static Message makeSetMetadataInt(uint8_t contextId, uint8_t field, int32_t value); - static Message makeTrackCreate(uint8_t contextId, uint16_t id, uint16_t insertId, uint16_t sourceId, const QString &title); - static Message makeTrackDelete(uint8_t contextId, uint16_t id); - static Message makeTrackOrder(uint8_t contextId, const QVector &tracks); - static Message makeTrackRetitle(uint8_t contextId, uint16_t id, const QString &title); - static Message makeTrustedUsers(uint8_t contextId, const QVector &users); - static Message makeUndo(uint8_t contextId, uint8_t overrideUser, bool redo); - static Message makeUndoDepth(uint8_t contextId, uint8_t depth); - static Message makeUndoPoint(uint8_t contextId); - static Message makeUserAcl(uint8_t contextId, const QVector &users); - static Message makeUserInfo(uint8_t contextId, uint8_t recipient, const QJsonDocument &msg); - - // Fills given message list with put image messages, potentially cropping - // the image if the given coordinates are negative and splitting it into - // multiple messages if it doesn't fit into a single one. - static void makePutImages(MessageList &msgs, uint8_t contextId, uint16_t layer, uint8_t mode, int x, int y, const QImage &image); - - static Message makeLocalChangeLayerVisibility(int layerId, bool hidden); - static Message makeLocalChangeBackgroundColor(const QColor &color); - static Message makeLocalChangeBackgroundClear(); - static Message makeLocalChangeViewMode(DP_ViewMode viewMode); - static Message makeLocalChangeActiveLayer(int layerId); - static Message makeLocalChangeActiveFrame(int frameIndex); - static Message makeLocalChangeOnionSkins(const DP_OnionSkins *oss); - static Message makeLocalChangeTrackVisibility(int trackId, bool hidden); - static Message makeLocalChangeTrackOnionSkin(int trackId, bool onionSkin); - - Message(); - Message(const Message &other); - Message(Message &&other); - - Message &operator=(const Message &other); - Message &operator=(Message &&other); - - ~Message(); - - DP_Message *get() const; - - bool isNull() const; - - DP_MessageType type() const; - QString typeName() const; - - unsigned int contextId() const; - - size_t length() const; - - DP_MsgServerCommand *toServerCommand() const; - DP_MsgData *toData() const; - - bool serialize(QByteArray &buffer) const; - - drawdance::Message makeBackwardCompatible() const; - - void setIndirectCompatFlag(); - - bool shouldSmoothe() - { - switch(type()) { - case DP_MSG_DRAW_DABS_CLASSIC: - case DP_MSG_DRAW_DABS_PIXEL: - case DP_MSG_DRAW_DABS_PIXEL_SQUARE: - case DP_MSG_DRAW_DABS_MYPAINT: - case DP_MSG_MOVE_POINTER: - return true; - default: - return false; - } - } - -private: - explicit Message(DP_Message *cs); - - static QByteArray compressAlphaMask(const QImage &mask); - - static void makePutImagesRecursive(MessageList &msgs, uint8_t contextId, uint16_t layer, uint8_t mode, int x, int y, const QImage &image, const QRect &bounds, int estimatedSize = 0); - - static unsigned char *getDeserializeBuffer(void *user, size_t size); - - static void setUchars(size_t size, unsigned char *out, void *user); - static void setUint8s(int count, uint8_t *out, void *user); - static void setUint16s(int count, uint16_t *out, void *user); - static void setClassicDabs(int count, DP_ClassicDab *out, void *user); - static void setPixelDabs(int count, DP_PixelDab *out, void *user); - - DP_Message *m_data; -}; - -} - -#endif diff --git a/src/libclient/drawdance/paintengine.cpp b/src/libclient/drawdance/paintengine.cpp index 4fdb29c246..521fdf176d 100644 --- a/src/libclient/drawdance/paintengine.cpp +++ b/src/libclient/drawdance/paintengine.cpp @@ -1,13 +1,11 @@ // SPDX-License-Identifier: GPL-3.0-or-later - extern "C" { #include #include } - -#include "libclient/drawdance/paintengine.h" #include "libclient/drawdance/aclstate.h" #include "libclient/drawdance/image.h" +#include "libclient/drawdance/paintengine.h" #include "libclient/drawdance/snapshotqueue.h" #include "libshared/util/paths.h" #include @@ -44,7 +42,7 @@ DP_PaintEngine *PaintEngine::get() return m_data; } -MessageList PaintEngine::reset( +net::MessageList PaintEngine::reset( AclState &acls, SnapshotQueue &sq, uint8_t localUserId, DP_RendererTileFn rendererTileFn, DP_RendererUnlockFn rendererUnlockFn, DP_RendererResizeFn rendererResizeFn, void *rendererUser, @@ -52,7 +50,7 @@ MessageList PaintEngine::reset( DP_PaintEngineDumpPlaybackFn dumpPlaybackFn, void *playbackUser, const CanvasState &canvasState, DP_Player *player) { - MessageList localResetImage; + net::MessageList localResetImage; DP_paint_engine_local_state_reset_image_build( m_data, pushResetMessage, &localResetImage); bool wantCanvasHistoryDump = @@ -76,7 +74,8 @@ int PaintEngine::renderThreadCount() const void PaintEngine::setLocalDrawingInProgress(bool localDrawingInProgress) { - DP_paint_engine_local_drawing_in_progress_set(m_data, localDrawingInProgress); + DP_paint_engine_local_drawing_in_progress_set( + m_data, localDrawingInProgress); } void PaintEngine::setWantCanvasHistoryDump(bool wantCanvasHistoryDump) @@ -134,7 +133,8 @@ Tile PaintEngine::localBackgroundTile() const RecordStartResult PaintEngine::makeRecorderParameters( const QString &path, const QString &writer, const QString &writerVersion, - const QString &type, DP_RecorderType &outRecorderType, JSON_Value *&outHeader) + const QString &type, DP_RecorderType &outRecorderType, + JSON_Value *&outHeader) { DP_RecorderType recorderType; if(path.endsWith(".dprec", Qt::CaseInsensitive)) { @@ -146,9 +146,8 @@ RecordStartResult PaintEngine::makeRecorderParameters( } JSON_Value *header = DP_recorder_header_new( - "writer", qUtf8Printable(writer), - "writerversion", qUtf8Printable(writerVersion), - "type", qUtf8Printable(type), NULL); + "writer", qUtf8Printable(writer), "writerversion", + qUtf8Printable(writerVersion), "type", qUtf8Printable(type), NULL); if(!header) { return RECORD_START_HEADER_ERROR; } @@ -171,7 +170,8 @@ RecordStartResult PaintEngine::startRecorder( } QByteArray pathBytes = path.toUtf8(); - if (DP_paint_engine_recorder_start(m_data, recorderType, header, pathBytes.constData())) { + if(DP_paint_engine_recorder_start( + m_data, recorderType, header, pathBytes.constData())) { return RECORD_START_SUCCESS; } else { return RECORD_START_OPEN_ERROR; @@ -179,7 +179,7 @@ RecordStartResult PaintEngine::startRecorder( } RecordStartResult PaintEngine::exportTemplate( - const QString &path, const drawdance::MessageList &snapshot, + const QString &path, const net::MessageList &snapshot, const QString &writer, const QString &writerVersion, const QString &type) { DP_RecorderType recorderType; @@ -191,17 +191,19 @@ RecordStartResult PaintEngine::exportTemplate( } QByteArray pathBytes = path.toUtf8(); - DP_Output *output = DP_file_output_save_new_from_path(pathBytes.constData()); + DP_Output *output = + DP_file_output_save_new_from_path(pathBytes.constData()); if(!output) { return RECORD_START_OPEN_ERROR; } - DP_Recorder *r = DP_recorder_new_inc(recorderType, header, nullptr, nullptr, nullptr, output); + DP_Recorder *r = DP_recorder_new_inc( + recorderType, header, nullptr, nullptr, nullptr, output); if(!r) { return RECORD_START_RECORDER_ERROR; } - for(const drawdance::Message &msg : snapshot) { + for(const net::Message &msg : snapshot) { if(!DP_recorder_message_push_inc(r, msg.get())) { break; } @@ -228,24 +230,30 @@ bool PaintEngine::recorderIsRecording() const return DP_paint_engine_recorder_is_recording(m_data); } -DP_PlayerResult PaintEngine::stepPlayback(long long steps, MessageList &outMsgs) +DP_PlayerResult +PaintEngine::stepPlayback(long long steps, net::MessageList &outMsgs) { - return DP_paint_engine_playback_step(m_data, steps, PaintEngine::pushMessage, &outMsgs); + return DP_paint_engine_playback_step( + m_data, steps, PaintEngine::pushMessage, &outMsgs); } -DP_PlayerResult PaintEngine::skipPlaybackBy(long long steps, bool bySnapshots, MessageList &outMsgs) +DP_PlayerResult PaintEngine::skipPlaybackBy( + long long steps, bool bySnapshots, net::MessageList &outMsgs) { DrawContext drawContext = DrawContextPool::acquire(); return DP_paint_engine_playback_skip_by( - m_data, drawContext.get(), steps, bySnapshots, PaintEngine::pushMessage, &outMsgs); + m_data, drawContext.get(), steps, bySnapshots, PaintEngine::pushMessage, + &outMsgs); } -DP_PlayerResult PaintEngine::jumpPlaybackTo(long long position, MessageList &outMsgs) +DP_PlayerResult +PaintEngine::jumpPlaybackTo(long long position, net::MessageList &outMsgs) { DrawContext drawContext = DrawContextPool::acquire(); return DP_paint_engine_playback_jump_to( - m_data, drawContext.get(), position, PaintEngine::pushMessage, &outMsgs); + m_data, drawContext.get(), position, PaintEngine::pushMessage, + &outMsgs); } DP_PlayerResult PaintEngine::beginPlayback() @@ -253,7 +261,8 @@ DP_PlayerResult PaintEngine::beginPlayback() return DP_paint_engine_playback_begin(m_data); } -DP_PlayerResult PaintEngine::playPlayback(long long msecs, MessageList &outMsgs) +DP_PlayerResult +PaintEngine::playPlayback(long long msecs, net::MessageList &outMsgs) { return DP_paint_engine_playback_play( m_data, msecs, PaintEngine::pushMessage, &outMsgs); @@ -296,36 +305,44 @@ QImage PaintEngine::playbackIndexThumbnailAt(size_t index) { bool error; QImage img = wrapImage( - DP_paint_engine_playback_index_thumbnail_at(m_data, index, &error)); - if (error) { + DP_paint_engine_playback_index_thumbnail_at(m_data, index, &error)); + if(error) { qWarning("Error in thumbnail at index %zu: %s", index, DP_error()); } return img; } -DP_PlayerResult PaintEngine::stepDumpPlayback(MessageList &outMsgs) +DP_PlayerResult PaintEngine::stepDumpPlayback(net::MessageList &outMsgs) { - return DP_paint_engine_playback_dump_step(m_data, PaintEngine::pushMessage, &outMsgs); + return DP_paint_engine_playback_dump_step( + m_data, PaintEngine::pushMessage, &outMsgs); } -DP_PlayerResult PaintEngine::jumpDumpPlaybackToPreviousReset(MessageList &outMsgs) +DP_PlayerResult +PaintEngine::jumpDumpPlaybackToPreviousReset(net::MessageList &outMsgs) { - return DP_paint_engine_playback_dump_jump_previous_reset(m_data, PaintEngine::pushMessage, &outMsgs); + return DP_paint_engine_playback_dump_jump_previous_reset( + m_data, PaintEngine::pushMessage, &outMsgs); } -DP_PlayerResult PaintEngine::jumpDumpPlaybackToNextReset(MessageList &outMsgs) +DP_PlayerResult +PaintEngine::jumpDumpPlaybackToNextReset(net::MessageList &outMsgs) { - return DP_paint_engine_playback_dump_jump_next_reset(m_data, PaintEngine::pushMessage, &outMsgs); + return DP_paint_engine_playback_dump_jump_next_reset( + m_data, PaintEngine::pushMessage, &outMsgs); } -DP_PlayerResult PaintEngine::jumpDumpPlayback(long long position, MessageList &outMsgs) +DP_PlayerResult +PaintEngine::jumpDumpPlayback(long long position, net::MessageList &outMsgs) { - return DP_paint_engine_playback_dump_jump(m_data, position, PaintEngine::pushMessage, &outMsgs); + return DP_paint_engine_playback_dump_jump( + m_data, position, PaintEngine::pushMessage, &outMsgs); } -bool PaintEngine::flushPlayback(MessageList &outMsgs) +bool PaintEngine::flushPlayback(net::MessageList &outMsgs) { - return DP_paint_engine_playback_flush(m_data, PaintEngine::pushMessage, &outMsgs); + return DP_paint_engine_playback_flush( + m_data, PaintEngine::pushMessage, &outMsgs); } bool PaintEngine::closePlayback() @@ -333,13 +350,17 @@ bool PaintEngine::closePlayback() return DP_paint_engine_playback_close(m_data); } -void PaintEngine::previewCut(int layerId, const QRect &bounds, const QImage &mask) +void PaintEngine::previewCut( + int layerId, const QRect &bounds, const QImage &mask) { - Q_ASSERT(mask.isNull() || mask.format() == QImage::Format_ARGB32_Premultiplied); + Q_ASSERT( + mask.isNull() || mask.format() == QImage::Format_ARGB32_Premultiplied); Q_ASSERT(mask.isNull() || mask.size() == bounds.size()); DP_paint_engine_preview_cut( - m_data, layerId, bounds.x(), bounds.y(), bounds.width(), bounds.height(), - mask.isNull() ? nullptr : reinterpret_cast(mask.constBits())); + m_data, layerId, bounds.x(), bounds.y(), bounds.width(), + bounds.height(), + mask.isNull() ? nullptr + : reinterpret_cast(mask.constBits())); } void PaintEngine::clearCutPreview() @@ -372,10 +393,10 @@ void PaintEngine::clearTransformPreview() DP_paint_engine_preview_clear(m_data, DP_PREVIEW_TRANSFORM); } -void PaintEngine::previewDabs(int layerId, int count, const drawdance::Message *msgs) +void PaintEngine::previewDabs(int layerId, int count, const net::Message *msgs) { DP_paint_engine_preview_dabs_inc( - m_data, layerId, count, drawdance::Message::asRawMessages(msgs)); + m_data, layerId, count, net::Message::asRawMessages(msgs)); } void PaintEngine::clearDabsPreview() @@ -385,17 +406,20 @@ void PaintEngine::clearDabsPreview() CanvasState PaintEngine::viewCanvasState() const { - return drawdance::CanvasState::noinc(DP_paint_engine_view_canvas_state_inc(m_data)); + return drawdance::CanvasState::noinc( + DP_paint_engine_view_canvas_state_inc(m_data)); } CanvasState PaintEngine::historyCanvasState() const { - return drawdance::CanvasState::noinc(DP_paint_engine_history_canvas_state_inc(m_data)); + return drawdance::CanvasState::noinc( + DP_paint_engine_history_canvas_state_inc(m_data)); } CanvasState PaintEngine::sampleCanvasState() const { - return drawdance::CanvasState::noinc(DP_paint_engine_sample_canvas_state_inc(m_data)); + return drawdance::CanvasState::noinc( + DP_paint_engine_sample_canvas_state_inc(m_data)); } QString PaintEngine::getDumpDir() @@ -410,8 +434,8 @@ long long PaintEngine::getTimeMs(void *) void PaintEngine::pushMessage(void *user, DP_Message *msg) { - MessageList *outMsgs = static_cast(user); - outMsgs->append(drawdance::Message::noinc(msg)); + net::MessageList *outMsgs = static_cast(user); + outMsgs->append(net::Message::noinc(msg)); } bool PaintEngine::pushResetMessage(void *user, DP_Message *msg) @@ -424,7 +448,7 @@ bool PaintEngine::shouldSnapshot(void *user) { static constexpr long long MESSAGE_INDEX_INTERVAL = 10000; BuildIndexParams *params = static_cast(user); - if( params->messagesSinceLastSnapshot++ > MESSAGE_INDEX_INTERVAL) { + if(params->messagesSinceLastSnapshot++ > MESSAGE_INDEX_INTERVAL) { params->messagesSinceLastSnapshot = 0; return true; } else { @@ -434,10 +458,10 @@ bool PaintEngine::shouldSnapshot(void *user) void PaintEngine::addLayerVisibleInFrame(void *user, int layerId, bool visible) { - if(visible) { - QSet *layersVisibleInFrame = static_cast *>(user); - layersVisibleInFrame->insert(layerId); - } + if(visible) { + QSet *layersVisibleInFrame = static_cast *>(user); + layersVisibleInFrame->insert(layerId); + } } void PaintEngine::indexProgress(void *user, int percent) diff --git a/src/libclient/drawdance/paintengine.h b/src/libclient/drawdance/paintengine.h index beab28a95b..e0c4cceee9 100644 --- a/src/libclient/drawdance/paintengine.h +++ b/src/libclient/drawdance/paintengine.h @@ -1,12 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-or-later - #ifndef DRAWDANCE_PAINTENGINE_H #define DRAWDANCE_PAINTENGINE_H - extern "C" { #include } - #include "libclient/drawdance/canvasstate.h" #include "libclient/drawdance/global.h" #include @@ -29,7 +26,7 @@ enum RecordStartResult { class PaintEngine { public: - using BuildIndexProgressFn = std::function; + using BuildIndexProgressFn = std::function; PaintEngine( AclState &acls, SnapshotQueue &sq, bool wantCanvasHistoryDump, @@ -48,7 +45,7 @@ class PaintEngine { DP_PaintEngine *get(); - MessageList reset( + net::MessageList reset( AclState &acls, SnapshotQueue &sq, uint8_t localUserId, DP_RendererTileFn rendererTileFn, DP_RendererUnlockFn rendererUnlockFn, DP_RendererResizeFn rendererResizeFn, void *rendererUser, @@ -81,35 +78,40 @@ class PaintEngine { Tile localBackgroundTile() const; static RecordStartResult makeRecorderParameters( - const QString &path, const QString &writer, const QString &writerVersion, - const QString &type, DP_RecorderType &outRecorderType, JSON_Value *&outHeader); + const QString &path, const QString &writer, + const QString &writerVersion, const QString &type, + DP_RecorderType &outRecorderType, JSON_Value *&outHeader); RecordStartResult startRecorder( const QString &path, const QString &writer, const QString &writerVersion, const QString &type); RecordStartResult exportTemplate( - const QString &path, const drawdance::MessageList &snapshot, - const QString &writer, const QString &writerVersion, const QString &type); + const QString &path, const net::MessageList &snapshot, + const QString &writer, const QString &writerVersion, + const QString &type); bool stopRecorder(); bool recorderIsRecording() const; - DP_PlayerResult stepPlayback(long long steps, MessageList &outMsgs); - DP_PlayerResult skipPlaybackBy(long long steps, bool bySnapshots, MessageList &outMsgs); - DP_PlayerResult jumpPlaybackTo(long long position, MessageList &outMsgs); + DP_PlayerResult stepPlayback(long long steps, net::MessageList &outMsgs); + DP_PlayerResult skipPlaybackBy( + long long steps, bool bySnapshots, net::MessageList &outMsgs); + DP_PlayerResult + jumpPlaybackTo(long long position, net::MessageList &outMsgs); DP_PlayerResult beginPlayback(); - DP_PlayerResult playPlayback(long long msecs, MessageList &outMsgs); + DP_PlayerResult playPlayback(long long msecs, net::MessageList &outMsgs); bool buildPlaybackIndex(BuildIndexProgressFn progressFn); bool loadPlaybackIndex(); unsigned int playbackIndexMessageCount(); size_t playbackIndexEntryCount(); QImage playbackIndexThumbnailAt(size_t index); - DP_PlayerResult stepDumpPlayback(MessageList &outMsgs); - DP_PlayerResult jumpDumpPlaybackToPreviousReset(MessageList &outMsgs); - DP_PlayerResult jumpDumpPlaybackToNextReset(MessageList &outMsgs); - DP_PlayerResult jumpDumpPlayback(long long position, MessageList &outMsgs); - bool flushPlayback(MessageList &outMsgs); + DP_PlayerResult stepDumpPlayback(net::MessageList &outMsgs); + DP_PlayerResult jumpDumpPlaybackToPreviousReset(net::MessageList &outMsgs); + DP_PlayerResult jumpDumpPlaybackToNextReset(net::MessageList &outMsgs); + DP_PlayerResult + jumpDumpPlayback(long long position, net::MessageList &outMsgs); + bool flushPlayback(net::MessageList &outMsgs); bool closePlayback(); void previewCut(int layerId, const QRect &bounds, const QImage &mask); @@ -118,7 +120,7 @@ class PaintEngine { int layerId, int x, int y, const QImage &img, const QPolygon &dstPolygon, int interpolation); void clearTransformPreview(); - void previewDabs(int layerId, int count, const Message *msgs); + void previewDabs(int layerId, int count, const net::Message *msgs); void clearDabsPreview(); CanvasState viewCanvasState() const; diff --git a/src/libclient/net/client.cpp b/src/libclient/net/client.cpp index 41a9e340f0..7ee0f288a0 100644 --- a/src/libclient/net/client.cpp +++ b/src/libclient/net/client.cpp @@ -1,18 +1,15 @@ // SPDX-License-Identifier: GPL-3.0-or-later - -#include "libclient/drawdance/message.h" #include "libclient/net/client.h" -#include "libclient/net/tcpserver.h" #include "libclient/net/login.h" -#include "libclient/net/servercmd.h" +#include "libclient/net/message.h" +#include "libclient/net/tcpserver.h" +#include "libshared/net/servercmd.h" #include "libshared/util/qtcompat.h" - +#include #ifdef Q_OS_ANDROID # include "libshared/util/androidutils.h" #endif -#include - namespace net { Client::Client(QObject *parent) @@ -42,52 +39,61 @@ void Client::connectToServer(int timeoutSecs, LoginHandler *loginhandler) .arg( reinterpret_cast(server), QT_POINTER_SIZE * 2, 16, QLatin1Char('0'))}; - m_wifiLock = new utils::AndroidWifiLock{"WIFI_MODE_FULL_LOW_LATENCY", tag}; + m_wifiLock = + new utils::AndroidWifiLock{"WIFI_MODE_FULL_LOW_LATENCY", tag}; } #endif connect(server, &TcpServer::loggingOut, this, &Client::serverDisconnecting); - connect(server, &TcpServer::serverDisconnected, this, &Client::handleDisconnect); - connect(server, &TcpServer::serverDisconnected, loginhandler, &LoginHandler::serverDisconnected); + connect( + server, &TcpServer::serverDisconnected, this, + &Client::handleDisconnect); + connect( + server, &TcpServer::serverDisconnected, loginhandler, + &LoginHandler::serverDisconnected); connect(server, &TcpServer::loggedIn, this, &Client::handleConnect); - connect(server, &TcpServer::messagesReceived, this, &Client::handleMessages); + connect( + server, &TcpServer::messagesReceived, this, &Client::handleMessages); connect(server, &TcpServer::bytesReceived, this, &Client::bytesReceived); connect(server, &TcpServer::bytesSent, this, &Client::bytesSent); connect(server, &TcpServer::lagMeasured, this, &Client::lagMeasured); - connect(server, &TcpServer::gracefullyDisconnecting, this, [this](MessageQueue::GracefulDisconnect reason, const QString &message) - { - if(reason == MessageQueue::GracefulDisconnect::Kick) { - emit youWereKicked(message); - return; - } + connect( + server, &TcpServer::gracefullyDisconnecting, this, + [this]( + MessageQueue::GracefulDisconnect reason, const QString &message) { + if(reason == MessageQueue::GracefulDisconnect::Kick) { + emit youWereKicked(message); + return; + } - QString chat; - switch(reason) { - case MessageQueue::GracefulDisconnect::Kick: - emit youWereKicked(message); - return; - case MessageQueue::GracefulDisconnect::Error: - chat = tr("A server error occurred!"); - break; - case MessageQueue::GracefulDisconnect::Shutdown: - chat = tr("The server is shutting down!"); - break; - default: - chat = "Unknown error"; - } + QString chat; + switch(reason) { + case MessageQueue::GracefulDisconnect::Kick: + emit youWereKicked(message); + return; + case MessageQueue::GracefulDisconnect::Error: + chat = tr("A server error occurred!"); + break; + case MessageQueue::GracefulDisconnect::Shutdown: + chat = tr("The server is shutting down!"); + break; + default: + chat = "Unknown error"; + } - if(!message.isEmpty()) - chat = QString("%1 (%2)").arg(chat, message); + if(!message.isEmpty()) + chat = QString("%1 (%2)").arg(chat, message); - emit serverMessage(chat, true); - }); + emit serverMessage(chat, true); + }); if(loginhandler->mode() == LoginHandler::Mode::HostRemote) loginhandler->setUserId(m_myId); - emit serverConnected(loginhandler->url().host(), loginhandler->url().port()); + emit serverConnected( + loginhandler->url().host(), loginhandler->url().port()); server->login(loginhandler); m_catchupTo = 0; @@ -123,7 +129,8 @@ void Client::handleConnect( emit serverLoggedIn(join, m_compatibilityMode, joinPassword); } -void Client::handleDisconnect(const QString &message,const QString &errorcode, bool localDisconnect) +void Client::handleDisconnect( + const QString &message, const QString &errorcode, bool localDisconnect) { Q_ASSERT(isConnected()); @@ -149,22 +156,24 @@ int Client::uploadQueueBytes() const } -void Client::sendMessage(const drawdance::Message &msg) +void Client::sendMessage(const net::Message &msg) { sendMessages(1, &msg); } -void Client::sendMessages(int count, const drawdance::Message *msgs) +void Client::sendMessages(int count, const net::Message *msgs) { if(m_compatibilityMode) { - QVector compatibleMsgs = filterCompatibleMessages(count, msgs); - sendCompatibleMessages(compatibleMsgs.count(), compatibleMsgs.constData()); + QVector compatibleMsgs = + filterCompatibleMessages(count, msgs); + sendCompatibleMessages( + compatibleMsgs.count(), compatibleMsgs.constData()); } else { sendCompatibleMessages(count, msgs); } } -void Client::sendCompatibleMessages(int count, const drawdance::Message *msgs) +void Client::sendCompatibleMessages(int count, const net::Message *msgs) { if(count > 0) { emit drawingCommandsLocal(count, msgs); @@ -179,22 +188,24 @@ void Client::sendCompatibleMessages(int count, const drawdance::Message *msgs) } } -void Client::sendResetMessage(const drawdance::Message &msg) +void Client::sendResetMessage(const net::Message &msg) { sendResetMessages(1, &msg); } -void Client::sendResetMessages(int count, const drawdance::Message *msgs) +void Client::sendResetMessages(int count, const net::Message *msgs) { if(m_compatibilityMode) { - QVector compatibleMsgs = filterCompatibleMessages(count, msgs); - sendCompatibleResetMessages(compatibleMsgs.count(), compatibleMsgs.constData()); + QVector compatibleMsgs = + filterCompatibleMessages(count, msgs); + sendCompatibleResetMessages( + compatibleMsgs.count(), compatibleMsgs.constData()); } else { sendCompatibleResetMessages(count, msgs); } } -void Client::sendCompatibleResetMessages(int count, const drawdance::Message *msgs) +void Client::sendCompatibleResetMessages(int count, const net::Message *msgs) { if(count > 0) { if(m_server) { @@ -205,16 +216,18 @@ void Client::sendCompatibleResetMessages(int count, const drawdance::Message *ms } } -QVector Client::filterCompatibleMessages(int count, const drawdance::Message *msgs) +QVector +Client::filterCompatibleMessages(int count, const net::Message *msgs) { // Ideally, the client shouldn't be attempting to send any incompatible // messages in the first place, but we'll err on the side of caution. In // particular, a thick server will kick us out if we send a wrong message. - QVector compatibleMsgs; + QVector compatibleMsgs; compatibleMsgs.reserve(count); for(int i = 0; i < count; ++i) { - const drawdance::Message &msg = msgs[i]; - const drawdance::Message compatibleMsg = msg.makeBackwardCompatible(); + const net::Message &msg = msgs[i]; + const net::Message compatibleMsg = + net::makeMessageBackwardCompatible(msg); if(compatibleMsg.isNull()) { qWarning("Incompatible %s message", qUtf8Printable(msg.typeName())); } else { @@ -224,10 +237,10 @@ QVector Client::filterCompatibleMessages(int count, const dr return compatibleMsgs; } -void Client::handleMessages(int count, drawdance::Message *msgs) +void Client::handleMessages(int count, net::Message *msgs) { for(int i = 0; i < count; ++i) { - drawdance::Message &msg = msgs[i]; + net::Message &msg = msgs[i]; switch(msg.type()) { case DP_MSG_SERVER_COMMAND: handleServerReply(ServerReply::fromMessage(msg)); @@ -248,10 +261,10 @@ void Client::handleMessages(int count, drawdance::Message *msgs) } emit messagesReceived(count, msgs); - // The server can send a "catchup" message when there is a significant number - // of messages queued. During login, we can show a progress bar and hide the canvas - // to speed up the initial catchup phase. - if(m_catchupTo>0) { + // The server can send a "catchup" message when there is a significant + // number of messages queued. During login, we can show a progress bar and + // hide the canvas to speed up the initial catchup phase. + if(m_catchupTo > 0) { m_caughtUp += count; if(m_caughtUp >= m_catchupTo) { qInfo("Catchup: caught up to %d messages", m_caughtUp); @@ -281,17 +294,21 @@ void Client::handleServerReply(const ServerReply &reply) case ServerReply::ReplyType::Alert: case ServerReply::ReplyType::Error: case ServerReply::ReplyType::Result: - emit serverMessage(reply.message, reply.type == ServerReply::ReplyType::Alert); + emit serverMessage( + reply.message, reply.type == ServerReply::ReplyType::Alert); break; case ServerReply::ReplyType::Log: { - QString time = QDateTime::fromString(reply.reply["timestamp"].toString(), Qt::ISODate).toLocalTime().toString(Qt::ISODate); + QString time = QDateTime::fromString( + reply.reply["timestamp"].toString(), Qt::ISODate) + .toLocalTime() + .toString(Qt::ISODate); QString user = reply.reply["user"].toString(); QString msg = reply.message; if(user.isEmpty()) emit serverLog(QStringLiteral("[%1] %2").arg(time, msg)); else emit serverLog(QStringLiteral("[%1] %2: %3").arg(time, user, msg)); - } break; + } break; case ServerReply::ReplyType::SessionConf: emit sessionConfChange(reply.reply["config"].toObject()); break; @@ -299,7 +316,8 @@ void Client::handleServerReply(const ServerReply &reply) // No longer used since 2.1.0. Replaced by RESETREQUEST break; case ServerReply::ReplyType::ResetRequest: - emit autoresetRequested(reply.reply["maxSize"].toInt(), reply.reply["query"].toBool()); + emit autoresetRequested( + reply.reply["maxSize"].toInt(), reply.reply["query"].toBool()); break; case ServerReply::ReplyType::Status: emit serverStatusUpdate(reply.reply["size"].toInt()); @@ -334,7 +352,7 @@ void Client::handleResetRequest(const ServerReply &msg) } } -void Client::handleData(const drawdance::Message &msg) +void Client::handleData(const net::Message &msg) { DP_MsgData *md = msg.toData(); if(md && DP_msg_data_recipient(md) == m_myId) { @@ -347,17 +365,18 @@ void Client::handleData(const drawdance::Message &msg) qWarning("Unknown data message type %d", type); break; } - } } -void Client::Client::handleUserInfo(const drawdance::Message &msg, DP_MsgData *md) +void Client::Client::handleUserInfo(const net::Message &msg, DP_MsgData *md) { size_t size; const unsigned char *bytes = DP_msg_data_body(md, &size); QJsonParseError err; - QJsonDocument json = QJsonDocument::fromJson(QByteArray::fromRawData( - reinterpret_cast(bytes), compat::castSize(size)), &err); + QJsonDocument json = QJsonDocument::fromJson( + QByteArray::fromRawData( + reinterpret_cast(bytes), compat::castSize(size)), + &err); if(json.isObject()) { QJsonObject info = json.object(); QString type = info["type"].toString(); @@ -369,7 +388,8 @@ void Client::Client::handleUserInfo(const drawdance::Message &msg, DP_MsgData *m qWarning("Unknown user info type '%s'", qUtf8Printable(type)); } } else { - qWarning("Could not parse JSON as an object: %s", + qWarning( + "Could not parse JSON as an object: %s", qUtf8Printable(err.errorString())); } } diff --git a/src/libclient/net/client.h b/src/libclient/net/client.h index 08ced31650..f6fe6624ea 100644 --- a/src/libclient/net/client.h +++ b/src/libclient/net/client.h @@ -1,10 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later - #ifndef DP_NET_CLIENT_H #define DP_NET_CLIENT_H - #include "libclient/net/server.h" - #include #include #include @@ -13,17 +10,14 @@ class QJsonObject; class QJsonArray; struct DP_MsgData; -namespace drawdance { - class Message; -} - namespace utils { - class AndroidWakeLock; - class AndroidWifiLock; +class AndroidWakeLock; +class AndroidWifiLock; } namespace net { +class Message; class LoginHandler; struct ServerReply; @@ -33,7 +27,7 @@ struct ServerReply; class Client final : public QObject { Q_OBJECT public: - explicit Client(QObject *parent=nullptr); + explicit Client(QObject *parent = nullptr); /** * @brief Connect to a remote server @@ -55,7 +49,7 @@ class Client final : public QObject { /** * Return the URL of the current (or last connected) session */ - QUrl sessionUrl(bool includeUser=false) const; + QUrl sessionUrl(bool includeUser = false) const; /** * @brief Is the client connected by network? @@ -65,7 +59,8 @@ class Client final : public QObject { /** * @brief Is the user connected and logged in? - * @return true if there is an active network connection and login process has completed + * @return true if there is an active network connection and login process + * has completed */ bool isLoggedIn() const { return m_server && m_server->isLoggedIn(); } @@ -77,34 +72,47 @@ class Client final : public QObject { /** * @brief Is this user a moderator? * - * Moderator status is a feature of the user account and cannot change during - * the connection. + * Moderator status is a feature of the user account and cannot change + * during the connection. */ bool isModerator() const { return m_moderator; } /** * @brief Get connection security level */ - Server::Security securityLevel() const { return m_server ? m_server->securityLevel() : Server::Security::NO_SECURITY; } + Server::Security securityLevel() const + { + return m_server ? m_server->securityLevel() + : Server::Security::NO_SECURITY; + } /** * @brief Get host certificate * * This is meaningful only if securityLevel != NO_SECURITY */ - QSslCertificate hostCertificate() const { return m_server ? m_server->hostCertificate() : QSslCertificate(); } + QSslCertificate hostCertificate() const + { + return m_server ? m_server->hostCertificate() : QSslCertificate(); + } /** * @brief Does the server support persistent sessions? * * TODO for version 3.0: Change this to sessionSupportsPersistence */ - bool serverSuppotsPersistence() const { return m_server && m_server->supportsPersistence(); } + bool serverSuppotsPersistence() const + { + return m_server && m_server->supportsPersistence(); + } /** * @brief Can the server receive abuse reports? */ - bool serverSupportsReports() const { return m_server && m_server->supportsAbuseReports(); } + bool serverSupportsReports() const + { + return m_server && m_server->supportsAbuseReports(); + } bool sessionSupportsAutoReset() const { return m_supportsAutoReset; } @@ -119,7 +127,10 @@ class Client final : public QObject { //! Are we expecting more incoming data? bool isFullyCaughtUp() const { return m_catchupTo == 0; } - int artificialLagMs() const { return m_server ? m_server->artificialLagMs() : 0; } + int artificialLagMs() const + { + return m_server ? m_server->artificialLagMs() : 0; + } void setArtificialLagMs(int msecs) { @@ -141,21 +152,22 @@ public slots: * * Just a convenience method around sendMessages. */ - void sendMessage(const drawdance::Message &msg); + void sendMessage(const net::Message &msg); /** * @brief Send messages to the server * - * A drawingCommandLocal signal will be emitted for drawing command messages. + * A drawingCommandLocal signal will be emitted for drawing command + * messages. */ - void sendMessages(int count, const drawdance::Message *msgs); + void sendMessages(int count, const net::Message *msgs); /** * @brief Send a single reset message to the server * * Just a convenience method around sendResetMessages. */ - void sendResetMessage(const drawdance::Message &msg); + void sendResetMessage(const net::Message &msg); /** * @brief Send the reset image to the server @@ -164,13 +176,13 @@ public slots: * will not be emitted, as the reset image was generate from content already * on the canvas. */ - void sendResetMessages(int count, const drawdance::Message *msgs); + void sendResetMessages(int count, const net::Message *msgs); void setSmoothDrainRate(int smoothDrainRate); signals: - void messagesReceived(int count, const drawdance::Message *msgs); - void drawingCommandsLocal(int count, const drawdance::Message *msgs); + void messagesReceived(int count, const net::Message *msgs); + void drawingCommandsLocal(int count, const net::Message *msgs); void catchupProgress(int percentage); void needSnapshot(); @@ -178,9 +190,11 @@ public slots: void sessionConfChange(const QJsonObject &config); void serverConnected(const QString &address, int port); - void serverLoggedIn(bool join, bool compatibilityMode, const QString &joinPassword); + void serverLoggedIn( + bool join, bool compatibilityMode, const QString &joinPassword); void serverDisconnecting(); - void serverDisconnected(const QString &message, const QString &errorcode, bool localDisconnect); + void serverDisconnected( + const QString &message, const QString &errorcode, bool localDisconnect); void youWereKicked(const QString &kickedBy); void serverMessage(const QString &message, bool isAlert); @@ -196,21 +210,24 @@ public slots: void userInfoReceived(int userId, const QJsonObject &info); private slots: - void handleMessages(int count, drawdance::Message *msgs); + void handleMessages(int count, net::Message *msgs); void handleConnect( const QUrl &url, uint8_t userid, bool join, bool auth, bool moderator, - bool supportsAutoReset, bool compatibilityMode, const QString &joinPassword); - void handleDisconnect(const QString &message, const QString &errorcode, bool localDisconnect); + bool supportsAutoReset, bool compatibilityMode, + const QString &joinPassword); + void handleDisconnect( + const QString &message, const QString &errorcode, bool localDisconnect); private: - void sendCompatibleMessages(int count, const drawdance::Message *msgs); - void sendCompatibleResetMessages(int count, const drawdance::Message *msgs); - QVector filterCompatibleMessages(int count, const drawdance::Message *msgs); + void sendCompatibleMessages(int count, const net::Message *msgs); + void sendCompatibleResetMessages(int count, const net::Message *msgs); + QVector + filterCompatibleMessages(int count, const net::Message *msgs); void handleServerReply(const ServerReply &msg); void handleResetRequest(const ServerReply &msg); - void handleData(const drawdance::Message &msg); - void handleUserInfo(const drawdance::Message &msg, DP_MsgData *md); + void handleData(const net::Message &msg); + void handleUserInfo(const net::Message &msg, DP_MsgData *md); Server *m_server = nullptr; #ifdef Q_OS_ANDROID diff --git a/src/libclient/net/login.cpp b/src/libclient/net/login.cpp index d90334af09..13224a881c 100644 --- a/src/libclient/net/login.cpp +++ b/src/libclient/net/login.cpp @@ -1,33 +1,30 @@ // SPDX-License-Identifier: GPL-3.0-or-later - #include "libclient/net/login.h" +#include "cmake-config/config.h" #include "libclient/net/loginsessions.h" #include "libclient/net/tcpserver.h" -#include "libclient/net/servercmd.h" #include "libclient/parentalcontrols/parentalcontrols.h" - -#include "cmake-config/config.h" #include "libshared/net/protover.h" +#include "libshared/net/servercmd.h" #include "libshared/util/networkaccess.h" #include "libshared/util/paths.h" - +#include #include -#include -#include -#include #include #include -#include -#include #include -#include +#include +#include #include +#include #include -#include +#include +#include #include +#include #ifndef NDEBUG -#define DEBUG_LOGIN +# define DEBUG_LOGIN #endif namespace { @@ -41,7 +38,8 @@ QFileInfo getCertFile(CertLocation location, const QString &hostname) else locationpath = "trusted-hosts/"; - return QFileInfo(utils::paths::writablePath(locationpath, hostname + ".pem")); + return QFileInfo( + utils::paths::writablePath(locationpath, hostname + ".pem")); } } @@ -67,7 +65,7 @@ LoginHandler::LoginHandler(Mode mode, const QUrl &url, QObject *parent) // Automatically join a session if the ID is included in the URL QString path = m_address.path(); - if(path.length()>1) { + if(path.length() > 1) { QRegularExpression idre("\\A/([a-zA-Z0-9:-]{1,64})/?\\z"); auto m = idre.match(path); if(m.hasMatch()) @@ -75,9 +73,7 @@ LoginHandler::LoginHandler(Mode mode, const QUrl &url, QObject *parent) } } -void LoginHandler::serverDisconnected() -{ -} +void LoginHandler::serverDisconnected() {} bool LoginHandler::receiveMessage(const ServerReply &msg) { @@ -99,25 +95,42 @@ bool LoginHandler::receiveMessage(const ServerReply &msg) handleError(msg.reply["code"].toString(), msg.message); return true; - } else if(msg.type != ServerReply::ReplyType::Login && msg.type != ServerReply::ReplyType::Result) { - qWarning() << "Login error: got reply type" << int(msg.type) << "when expected LOGIN, RESULT or ERROR"; + } else if( + msg.type != ServerReply::ReplyType::Login && + msg.type != ServerReply::ReplyType::Result) { + qWarning() << "Login error: got reply type" << int(msg.type) + << "when expected LOGIN, RESULT or ERROR"; failLogin(tr("Invalid state")); return true; } switch(m_state) { - case EXPECT_HELLO: expectHello(msg); break; - case EXPECT_STARTTLS: expectStartTls(msg); break; + case EXPECT_HELLO: + expectHello(msg); + break; + case EXPECT_STARTTLS: + expectStartTls(msg); + break; case WAIT_FOR_LOGIN_PASSWORD: case WAIT_FOR_EXTAUTH: - expectNothing(); break; - case EXPECT_IDENTIFIED: expectIdentified(msg); break; - case EXPECT_SESSIONLIST_TO_JOIN: expectSessionDescriptionJoin(msg); break; - case EXPECT_SESSIONLIST_TO_HOST: expectSessionDescriptionHost(msg); break; + expectNothing(); + break; + case EXPECT_IDENTIFIED: + expectIdentified(msg); + break; + case EXPECT_SESSIONLIST_TO_JOIN: + expectSessionDescriptionJoin(msg); + break; + case EXPECT_SESSIONLIST_TO_HOST: + expectSessionDescriptionHost(msg); + break; case WAIT_FOR_JOIN_PASSWORD: - case EXPECT_LOGIN_OK: return expectLoginOk(msg); break; - case ABORT_LOGIN: /* ignore messages in this state */ break; + case EXPECT_LOGIN_OK: + return expectLoginOk(msg); + break; + case ABORT_LOGIN: /* ignore messages in this state */ + break; } return true; @@ -132,7 +145,8 @@ void LoginHandler::expectNothing() void LoginHandler::expectHello(const ServerReply &msg) { if(msg.type != ServerReply::ReplyType::Login) { - qWarning() << "Login error. Greeting type is not LOGIN:" << int(msg.type); + qWarning() << "Login error. Greeting type is not LOGIN:" + << int(msg.type); failLogin(tr("Incompatible server")); return; } @@ -160,7 +174,8 @@ void LoginHandler::expectHello(const ServerReply &msg) } else if(flag == "TLS") { startTls = true; } else if(flag == "SECURE") { - // Changed in 2.1.9 (although in practice we've always done this): this flag is implied by TLS + // Changed in 2.1.9 (although in practice we've always done this): + // this flag is implied by TLS } else if(flag == "PERSIST") { m_canPersist = true; } else if(flag == "NOGUEST") { @@ -226,7 +241,8 @@ void LoginHandler::prepareToSendIdentity() if(m_mustAuth) prompt = tr("This server does not allow guest logins"); else - prompt = tr("Password needed to log in as \"%1\"").arg(m_address.userName()); + prompt = tr("Password needed to log in as \"%1\"") + .arg(m_address.userName()); emit loginNeeded(m_address.userName(), prompt); @@ -242,7 +258,8 @@ void LoginHandler::selectAvatar(const QImage &avatar) m_avatar = a.buffer().toBase64(); } -void LoginHandler::selectIdentity(const QString &username, const QString &password) +void LoginHandler::selectIdentity( + const QString &username, const QString &password) { m_address.setUserName(username); m_address.setPassword(password); @@ -270,7 +287,8 @@ void LoginHandler::sendIdentity() send("ident", args, kwargs, containsAvatar); } -void LoginHandler::requestExtAuth(const QString &username, const QString &password) +void LoginHandler::requestExtAuth( + const QString &username, const QString &password) { // Construct request body QJsonObject o; @@ -287,7 +305,8 @@ void LoginHandler::requestExtAuth(const QString &username, const QString &passwo QNetworkRequest req(m_extAuthUrl); req.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - QNetworkReply *reply = networkaccess::getInstance()->post(req, QJsonDocument(o).toJson()); + QNetworkReply *reply = + networkaccess::getInstance()->post(req, QJsonDocument(o).toJson()); connect(reply, &QNetworkReply::finished, this, [reply, this]() { reply->deleteLater(); @@ -296,7 +315,8 @@ void LoginHandler::requestExtAuth(const QString &username, const QString &passwo return; } QJsonParseError error; - const QJsonDocument doc = QJsonDocument::fromJson(reply->readAll(), &error); + const QJsonDocument doc = + QJsonDocument::fromJson(reply->readAll(), &error); if(error.error != QJsonParseError::NoError) { failLogin(tr("Auth server error: %1").arg(error.errorString())); return; @@ -306,7 +326,7 @@ void LoginHandler::requestExtAuth(const QString &username, const QString &passwo const QString status = obj["status"].toString(); if(status == "auth") { m_state = EXPECT_IDENTIFIED; - send("ident", { m_address.userName() }, {{"extauth", obj["token"]}}); + send("ident", {m_address.userName()}, {{"extauth", obj["token"]}}); emit extAuthComplete(true); @@ -341,12 +361,17 @@ void LoginHandler::expectIdentified(const ServerReply &msg) m_supportsExtAuthAvatars = msg.reply["avatar"].toBool(); if(!m_extAuthUrl.isValid()) { - qWarning("Invalid ext-auth URL: %s", qPrintable(msg.reply["extauthurl"].toString())); + qWarning( + "Invalid ext-auth URL: %s", + qPrintable(msg.reply["extauthurl"].toString())); failLogin(tr("Server misconfiguration: invalid ext-auth URL")); return; } - if(m_extAuthUrl.scheme() != "http" && m_extAuthUrl.scheme() != "https") { - qWarning("Unsupported ext-auth URL: %s", qPrintable(msg.reply["extauthurl"].toString())); + if(m_extAuthUrl.scheme() != "http" && + m_extAuthUrl.scheme() != "https") { + qWarning( + "Unsupported ext-auth URL: %s", + qPrintable(msg.reply["extauthurl"].toString())); failLogin(tr("Unsupported ext-auth URL scheme")); return; } @@ -434,7 +459,8 @@ void LoginHandler::expectSessionDescriptionJoin(const ServerReply &msg) const QJsonObject js = jsv.toObject(); protocol::ProtocolVersion protoVer = - protocol::ProtocolVersion::fromString(js["protocol"].toString()); + protocol::ProtocolVersion::fromString( + js["protocol"].toString()); QString incompatibleSeries; if(!protoVer.isCompatible()) { @@ -449,7 +475,7 @@ void LoginHandler::expectSessionDescriptionJoin(const ServerReply &msg) } } - const LoginSession session { + const LoginSession session{ js["id"].toString(), js["alias"].toString(), js["title"].toString(), @@ -461,17 +487,18 @@ void LoginHandler::expectSessionDescriptionJoin(const ServerReply &msg) js["persistent"].toBool(), js["closed"].toBool(), js["authOnly"].toBool() && m_isGuest, - js["nsfm"].toBool() - }; + js["nsfm"].toBool()}; m_sessions->updateSession(session); - if(!m_autoJoinId.isEmpty() && (session.id == m_autoJoinId || session.alias == m_autoJoinId)) { + if(!m_autoJoinId.isEmpty() && + (session.id == m_autoJoinId || session.alias == m_autoJoinId)) { // A session ID was given as part of the URL auto canJoin = session.incompatibleSeries.isEmpty(); - if (canJoin && pclevel >= parentalcontrols::Level::NoJoin) { - canJoin = !session.nsfm && !parentalcontrols::isNsfmTitle(session.title); + if(canJoin && pclevel >= parentalcontrols::Level::NoJoin) { + canJoin = !session.nsfm && + !parentalcontrols::isNsfmTitle(session.title); } if(canJoin) { @@ -500,14 +527,18 @@ void LoginHandler::expectSessionDescriptionJoin(const ServerReply &msg) if(session.id.isEmpty()) { failLogin(tr("Session not yet started!")); return; - } else if(parentalcontrols::level() >= parentalcontrols::Level::NoJoin) { - const auto blocked = session.nsfm || parentalcontrols::isNsfmTitle(session.title); - if (blocked) { + } else if( + parentalcontrols::level() >= parentalcontrols::Level::NoJoin) { + const auto blocked = + session.nsfm || parentalcontrols::isNsfmTitle(session.title); + if(blocked) { failLogin(tr("Blocked by parental controls")); return; } } else if(!session.incompatibleSeries.isEmpty()) { - failLogin(tr("Session for a different Drawpile version (%1) in progress!").arg(session.incompatibleSeries)); + failLogin( + tr("Session for a different Drawpile version (%1) in progress!") + .arg(session.incompatibleSeries)); return; } @@ -538,14 +569,16 @@ bool LoginHandler::expectLoginOk(const ServerReply &msg) const int userid = msg.reply["join"].toObject()["user"].toInt(); if(userid < 1 || userid > 254) { - qWarning() << "Login error. User ID" << userid << "out of supported range."; + qWarning() << "Login error. User ID" << userid + << "out of supported range."; failLogin(tr("Incompatible server")); return true; } m_userid = uint8_t(userid); - const QJsonArray sessionFlags = msg.reply["join"].toObject()["flags"].toArray(); + const QJsonArray sessionFlags = + msg.reply["join"].toObject()["flags"].toArray(); for(const QJsonValue &val : sessionFlags) { if(val.isString()) m_sessionFlags << val.toString(); @@ -566,11 +599,13 @@ bool LoginHandler::expectLoginOk(const ServerReply &msg) send("sessionconf", {}, kwargs); if(!m_announceUrl.isEmpty()) - m_server->sendMessage(ServerCommand::makeAnnounce(m_announceUrl, false)); + m_server->sendMessage( + ServerCommand::makeAnnounce(m_announceUrl, false)); // Upload initial session content if(m_mode == Mode::HostRemote) { - m_server->sendMessages(m_initialState.count(), m_initialState.constData()); + m_server->sendMessages( + m_initialState.count(), m_initialState.constData()); send("init-complete"); } } @@ -580,7 +615,8 @@ bool LoginHandler::expectLoginOk(const ServerReply &msg) } else { // Unexpected response - qWarning() << "Login error. Unexpected response while waiting for OK:" << msg.reply; + qWarning() << "Login error. Unexpected response while waiting for OK:" + << msg.reply; failLogin(tr("Incompatible server")); } @@ -624,22 +660,24 @@ void LoginHandler::sendJoinCommand() kwargs["password"] = m_joinPassword; } - send("join", { m_selectedId }, kwargs); + send("join", {m_selectedId}, kwargs); m_state = EXPECT_LOGIN_OK; } void LoginHandler::reportSession(const QString &id, const QString &reason) { - send("report", {}, { - {"session", id}, - {"reason", reason} - }); + send("report", {}, {{"session", id}, {"reason", reason}}); } void LoginHandler::startTls() { - connect(m_server->m_socket, &QSslSocket::encrypted, this, &LoginHandler::tlsStarted); - connect(m_server->m_socket, QOverload&>::of(&QSslSocket::sslErrors), this, &LoginHandler::tlsError); + connect( + m_server->m_socket, &QSslSocket::encrypted, this, + &LoginHandler::tlsStarted); + connect( + m_server->m_socket, + QOverload &>::of(&QSslSocket::sslErrors), this, + &LoginHandler::tlsError); m_server->m_socket->startClientEncryption(); } @@ -650,8 +688,8 @@ void LoginHandler::tlsError(const QList &errors) QString errorstr; bool fail = false; - // TODO this was optimized for self signed certificates back end Let's Encrypt - // didn't exist. This should be fixed to better support actual CAs. + // TODO this was optimized for self signed certificates back end Let's + // Encrypt didn't exist. This should be fixed to better support actual CAs. bool isIp = QHostAddress().setAddress(m_address.host()); bool isSelfSigned = m_server->hostCertificate().isSelfSigned(); qDebug() << errors.size() << "SSL error(s), self-signed" << isSelfSigned; @@ -664,12 +702,14 @@ void LoginHandler::tlsError(const QList &errors) ignore << e; } else if(isIp && e.error() == QSslError::HostNameMismatch) { - // Ignore CN mismatch when using an IP address rather than a hostname + // Ignore CN mismatch when using an IP address rather than a + // hostname ignore << e; qInfo() << "Ignoring error about hostname mismatch with IP address:" << int(e.error()) << e.errorString(); - } else if(e.error() == QSslError::CertificateUntrusted && isSelfSigned) { + } else if( + e.error() == QSslError::CertificateUntrusted && isSelfSigned) { // "The root CA certificate is not trusted for this purpose" is an // error that spontaneously manifested in macOS and then way later // in Windows. We ignore it on self-signed certificates. @@ -715,7 +755,8 @@ void LoginHandler::tlsStarted() m_certFile = getCertFile(TRUSTED_HOSTS, hostname); if(m_certFile.exists()) { - QList trustedcerts = QSslCertificate::fromPath(m_certFile.absoluteFilePath()); + QList trustedcerts = + QSslCertificate::fromPath(m_certFile.absoluteFilePath()); if(trustedcerts.isEmpty() || trustedcerts.at(0).isNull()) { failLogin(tr("Invalid SSL certificate for host %1").arg(hostname)); @@ -735,7 +776,8 @@ void LoginHandler::tlsStarted() // Okay, not a trusted certificate, but check if we've seen it before m_certFile = getCertFile(KNOWN_HOSTS, hostname); if(m_certFile.exists()) { - QList knowncerts = QSslCertificate::fromPath(m_certFile.absoluteFilePath()); + QList knowncerts = + QSslCertificate::fromPath(m_certFile.absoluteFilePath()); if(knowncerts.isEmpty() || knowncerts.at(0).isNull()) { failLogin(tr("Invalid SSL certificate for host %1").arg(hostname)); @@ -791,7 +833,8 @@ void LoginHandler::handleError(const QString &code, const QString &msg) error = tr("Session not found!"); else if(code == "badPassword") { if(m_passwordState == WAIT_FOR_LOGIN_PASSWORD) { - error = tr("Incorrect password for '%1'!").arg(m_address.userName()); + error = + tr("Incorrect password for '%1'!").arg(m_address.userName()); emit badLoginPassword(); } else { error = tr("Incorrect session password!"); @@ -803,7 +846,8 @@ void LoginHandler::handleError(const QString &code, const QString &msg) else if(code == "nameInUse") error = tr("Username already taken!"); else if(code == "closed") - error = m_mode == Mode::Join ? tr("Session is closed!") : tr("Server is full!"); + error = m_mode == Mode::Join ? tr("Session is closed!") + : tr("Server is full!"); else if(code == "unauthorizedHost") error = tr("Hosting not authorized"); else if(code == "banned") @@ -826,8 +870,8 @@ void LoginHandler::send( const QString &cmd, const QJsonArray &args, const QJsonObject &kwargs, bool containsAvatar) { - ServerCommand sc { cmd, args, kwargs }; - drawdance::Message msg = sc.toMessage(); + ServerCommand sc{cmd, args, kwargs}; + net::Message msg = sc.toMessage(); if(msg.isNull() && containsAvatar) { qWarning("Removing avatar from server command and trying again"); sc.kwargs.remove("avatar"); @@ -876,8 +920,7 @@ QString LoginHandler::getSid() QSettings cfg1{ QSettings::NativeFormat, QSettings::UserScope, QStringLiteral("drawpile"), QStringLiteral("sid")}; - QSettings cfg2{ - QSettings::NativeFormat, QSettings::UserScope, org, app}; + QSettings cfg2{QSettings::NativeFormat, QSettings::UserScope, org, app}; bool haveSid1 = cfg1.contains(key1); bool haveSid2 = cfg2.contains(key2); diff --git a/src/libclient/net/login.h b/src/libclient/net/login.h index 5cea0c5dab..02df6fb83e 100644 --- a/src/libclient/net/login.h +++ b/src/libclient/net/login.h @@ -1,19 +1,16 @@ // SPDX-License-Identifier: GPL-3.0-or-later - #ifndef DP_CLIENT_NET_LOGINHANDLER_H #define DP_CLIENT_NET_LOGINHANDLER_H - -#include -#include -#include -#include -#include +#include "libclient/net/message.h" +#include "libshared/net/protover.h" #include +#include #include #include - -#include "libclient/drawdance/message.h" -#include "libshared/net/protover.h" +#include +#include +#include +#include class QImage; @@ -29,24 +26,30 @@ struct ServerReply; * * See also LoginHandler in src/shared/server/ for the serverside implementation * - * In some situations, the user must prompted for decisions (e.g. password is needed - * or user must decide which session to join.) In these situations, a signal is emitted. + * In some situations, the user must prompted for decisions (e.g. password is + * needed or user must decide which session to join.) In these situations, a + * signal is emitted. */ class LoginHandler final : public QObject { Q_OBJECT public: - enum class Mode {HostRemote, HostBuiltin, Join}; + enum class Mode { HostRemote, HostBuiltin, Join }; - LoginHandler(Mode mode, const QUrl &url, QObject *parent=nullptr); + LoginHandler(Mode mode, const QUrl &url, QObject *parent = nullptr); /** * @brief Set the desired user ID * - * Only for host mode. When joining an existing session, the server assigns the user ID. + * Only for host mode. When joining an existing session, the server assigns + * the user ID. * * @param userid */ - void setUserId(uint8_t userid) { Q_ASSERT(m_mode!=Mode::Join); m_userid=userid; } + void setUserId(uint8_t userid) + { + Q_ASSERT(m_mode != Mode::Join); + m_userid = userid; + } /** * @brief Set desired session ID alias @@ -54,7 +57,11 @@ class LoginHandler final : public QObject { * Only in host mode. * @param id */ - void setSessionAlias(const QString &alias) { Q_ASSERT(m_mode!=Mode::Join); m_sessionAlias=alias; } + void setSessionAlias(const QString &alias) + { + Q_ASSERT(m_mode != Mode::Join); + m_sessionAlias = alias; + } /** * @brief Set the session password @@ -63,7 +70,11 @@ class LoginHandler final : public QObject { * * @param password */ - void setPassword(const QString &password) { Q_ASSERT(m_mode!=Mode::Join); m_sessionPassword=password; } + void setPassword(const QString &password) + { + Q_ASSERT(m_mode != Mode::Join); + m_sessionPassword = password; + } /** * @brief Set the session title @@ -72,7 +83,11 @@ class LoginHandler final : public QObject { * * @param title */ - void setTitle(const QString &title) { Q_ASSERT(m_mode!=Mode::Join); m_title=title; } + void setTitle(const QString &title) + { + Q_ASSERT(m_mode != Mode::Join); + m_title = title; + } /** * @brief Set the initial session content to upload to the server @@ -81,21 +96,33 @@ class LoginHandler final : public QObject { * * @param msgs */ - void setInitialState(const drawdance::MessageList &msgs) { Q_ASSERT(m_mode==Mode::HostRemote); m_initialState = msgs; } + void setInitialState(const net::MessageList &msgs) + { + Q_ASSERT(m_mode == Mode::HostRemote); + m_initialState = msgs; + } /** * @brief Set session announcement URL * * Only for host mode. */ - void setAnnounceUrl(const QString &url) { Q_ASSERT(m_mode!=Mode::Join); m_announceUrl = url; } + void setAnnounceUrl(const QString &url) + { + Q_ASSERT(m_mode != Mode::Join); + m_announceUrl = url; + } /** * @brief Set session NSFM flag * * Only for host mode. */ - void setNsfm(bool nsfm) { Q_ASSERT(m_mode!=Mode::Join); m_nsfm = nsfm; } + void setNsfm(bool nsfm) + { + Q_ASSERT(m_mode != Mode::Join); + m_nsfm = nsfm; + } /** * @brief Set the server we're communicating with @@ -224,7 +251,8 @@ public slots: /** * @brief Actually join the session that the user selected. * - * Call this after confirming that the user really wants to join the selected session. + * Call this after confirming that the user really wants to join the + * selected session. */ void confirmJoinSelectedSession(); @@ -256,7 +284,8 @@ public slots: * Proceed by calling selectIdentity(username, QString()) * (omit password at this point to attempt a guest login) * - * @param canSelectCustomAvatar is true if the server has announced that it accepts custom avatars + * @param canSelectCustomAvatar is true if the server has announced that it + * accepts custom avatars */ void usernameNeeded(bool canSelectCustomAvatar); @@ -270,13 +299,14 @@ public slots: * @param nsfm if the session is marked NSFM * @param autoJoin if this was an automatic join or if the user picked it */ - void sessionConfirmationNeeded(const QString &title, bool nsfm, bool autoJoin); + void + sessionConfirmationNeeded(const QString &title, bool nsfm, bool autoJoin); /** * @brief The user must enter a password to proceed * - * This is emitted when attempting to join a session that is password protected. - * After the user has made a decision, call either + * This is emitted when attempting to join a session that is password + * protected. After the user has made a decision, call either * sendSessionPassword(password) to proceed or cancelLogin() to exit. * */ @@ -289,7 +319,8 @@ public slots: * either because the account is protected or because guest * logins are not allowed. * - * Proceed by calling either selectIdentity(username, password) or cancelLogin() + * Proceed by calling either selectIdentity(username, password) or + * cancelLogin() * * @param prompt prompt text */ @@ -340,7 +371,8 @@ public slots: * * Call acceptServerCertificate() or cancelLogin() to proceed() */ - void certificateCheckNeeded(const QSslCertificate &newCert, const QSslCertificate &oldCert); + void certificateCheckNeeded( + const QSslCertificate &newCert, const QSslCertificate &oldCert); /** * @brief Server title has changed @@ -349,7 +381,8 @@ public slots: void serverTitleChanged(const QString &title); private slots: - void failLogin(const QString &message, const QString &errorcode=QString()); + void + failLogin(const QString &message, const QString &errorcode = QString()); void tlsStarted(); void tlsError(const QList &errors); @@ -367,8 +400,8 @@ private slots: ABORT_LOGIN }; - void send - (const QString &cmd, const QJsonArray &args = QJsonArray(), + void send( + const QString &cmd, const QJsonArray &args = QJsonArray(), const QJsonObject &kwargs = QJsonObject(), bool containsAvatar = false); void expectNothing(); @@ -404,7 +437,7 @@ private slots: QString m_title; QString m_announceUrl; bool m_nsfm; - drawdance::MessageList m_initialState; + net::MessageList m_initialState; // Settings for joining QString m_joinPassword; diff --git a/src/libclient/net/message.cpp b/src/libclient/net/message.cpp new file mode 100644 index 0000000000..4d97a65031 --- /dev/null +++ b/src/libclient/net/message.cpp @@ -0,0 +1,715 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +extern "C" { +#include +#include +} +#include "libclient/canvas/blendmodes.h" +#include "libclient/drawdance/global.h" +#include "libclient/drawdance/tile.h" +#include "libclient/net/message.h" +#include "libshared/util/qtcompat.h" +#include +#include +#include +#include +#include + +namespace net { + +Message makeAnnotationCreateMessage( + unsigned int contextId, uint16_t id, int32_t x, int32_t y, uint16_t w, + uint16_t h) +{ + return Message::noinc( + DP_msg_annotation_create_new(contextId, id, x, y, w, h)); +} + +Message makeAnnotationDeleteMessage(uint8_t contextId, uint16_t id) +{ + return Message::noinc(DP_msg_annotation_delete_new(contextId, id)); +} + +Message makeAnnotationEditMessage( + uint8_t contextId, uint16_t id, uint32_t bg, uint8_t flags, uint8_t border, + const QString &text) +{ + QByteArray bytes = text.toUtf8(); + return Message::noinc(DP_msg_annotation_edit_new( + contextId, id, bg, flags, border, bytes.constData(), bytes.length())); +} + +Message makeAnnotationReshapeMessage( + uint8_t contextId, uint16_t id, int32_t x, int32_t y, uint16_t w, + uint16_t h) +{ + return Message::noinc( + DP_msg_annotation_reshape_new(contextId, id, x, y, w, h)); +} + +Message makeCanvasBackgroundMessage(uint8_t contextId, const QColor &color) +{ + uint32_t c = qToBigEndian(color.rgba()); + return Message::noinc(DP_msg_canvas_background_new( + contextId, Message::setUchars, sizeof(c), &c)); +} + +Message makeCanvasResizeMessage( + uint8_t contextId, int32_t top, int32_t right, int32_t bottom, int32_t left) +{ + return Message::noinc( + DP_msg_canvas_resize_new(contextId, top, right, bottom, left)); +} + +Message makeDefaultLayerMessage(uint8_t contextId, uint16_t id) +{ + return Message::noinc(DP_msg_default_layer_new(contextId, id)); +} + +Message makeFeatureAccessLevelsMessage( + uint8_t contextId, int featureCount, const uint8_t *features) +{ + return Message::noinc(DP_msg_feature_access_levels_new( + contextId, Message::setUint8s, featureCount, + const_cast(features))); +} + +Message makeFillRectMessage( + uint8_t contextId, uint16_t layer, uint8_t mode, uint32_t x, uint32_t y, + uint32_t w, uint32_t h, const QColor &color) +{ + return Message::noinc( + DP_msg_fill_rect_new(contextId, layer, mode, x, y, w, h, color.rgba())); +} + +Message makeInternalCatchupMessage(uint8_t contextId, int progress) +{ + return Message::noinc(DP_msg_internal_catchup_new(contextId, progress)); +} + +Message makeInternalCleanupMessage(uint8_t contextId) +{ + return Message::noinc(DP_msg_internal_cleanup_new(contextId)); +} + +Message makeInternalResetMessage(uint8_t contextId) +{ + return Message::noinc(DP_msg_internal_reset_new(contextId)); +} + +Message makeInternalSnapshotMessage(uint8_t contextId) +{ + return Message::noinc(DP_msg_internal_snapshot_new(contextId)); +} + +Message makeKeyFrameLayerAttributesMessage( + uint8_t contextId, uint16_t trackId, uint16_t frameIndex, + const QVector &layers) +{ + return Message::noinc(DP_msg_key_frame_layer_attributes_new( + contextId, trackId, frameIndex, Message::setUint16s, layers.size(), + const_cast(layers.constData()))); +} + +Message makeKeyFrameSetMessage( + uint8_t contextId, uint16_t trackId, uint16_t frameIndex, uint16_t sourceId, + uint16_t sourceIndex, uint8_t source) +{ + return Message::noinc(DP_msg_key_frame_set_new( + contextId, trackId, frameIndex, sourceId, sourceIndex, source)); +} + +Message makeKeyFrameRetitleMessage( + uint8_t contextId, uint16_t trackId, uint16_t frameIndex, + const QString &title) +{ + QByteArray bytes = title.toUtf8(); + return Message::noinc(DP_msg_key_frame_retitle_new( + contextId, trackId, frameIndex, bytes.constData(), bytes.length())); +} + +Message makeKeyFrameDeleteMessage( + uint8_t contextId, uint16_t trackId, uint16_t frameIndex, + uint16_t moveTrackId, uint16_t moveFrameIndex) +{ + return Message::noinc(DP_msg_key_frame_delete_new( + contextId, trackId, frameIndex, moveTrackId, moveFrameIndex)); +} + +Message +makeLaserTrailMessage(uint8_t contextId, uint32_t color, uint8_t persistence) +{ + return Message::noinc( + DP_msg_laser_trail_new(contextId, color, persistence)); +} + +Message makeLayerAttributesMessage( + uint8_t contextId, uint16_t id, uint8_t sublayer, uint8_t flags, + uint8_t opacity, uint8_t blend) +{ + return Message::noinc(DP_msg_layer_attributes_new( + contextId, id, sublayer, flags, opacity, blend)); +} + +Message makeLayerAclMessage( + uint8_t contextId, uint16_t id, uint8_t flags, + const QVector &exclusive) +{ + return Message::noinc(DP_msg_layer_acl_new( + contextId, id, flags, Message::setUint8s, exclusive.count(), + const_cast(exclusive.constData()))); +} + +Message makeLayerCreateMessage( + uint8_t contextId, uint16_t id, uint16_t source, uint32_t fill, + uint8_t flags, const QString &name) +{ + QByteArray bytes = name.toUtf8(); + return Message::noinc(DP_msg_layer_create_new( + contextId, id, source, fill, flags, bytes.constData(), bytes.length())); +} + +Message makeLayerTreeCreateMessage( + uint8_t contextId, uint16_t id, uint16_t source, uint16_t target, + uint32_t fill, uint8_t flags, const QString &name) +{ + QByteArray bytes = name.toUtf8(); + return Message::noinc(DP_msg_layer_tree_create_new( + contextId, id, source, target, fill, flags, bytes.constData(), + bytes.length())); +} + +Message makeLayerDeleteMessage(uint8_t contextId, uint16_t id, bool merge) +{ + return Message::noinc(DP_msg_layer_delete_new(contextId, id, merge)); +} + +Message +makeLayerTreeDeleteMessage(uint8_t contextId, uint16_t id, uint16_t mergeTo) +{ + return Message::noinc(DP_msg_layer_tree_delete_new(contextId, id, mergeTo)); +} + +Message makeLayerTreeMoveMessage( + uint8_t contextId, uint16_t layer, uint16_t parent, uint16_t sibling) +{ + return Message::noinc( + DP_msg_layer_tree_move_new(contextId, layer, parent, sibling)); +} + +Message +makeLayerRetitleMessage(uint8_t contextId, uint16_t id, const QString &title) +{ + QByteArray bytes = title.toUtf8(); + return Message::noinc(DP_msg_layer_retitle_new( + contextId, id, bytes.constData(), bytes.length())); +} + +Message makeMovePointerMessage(uint8_t contextId, int32_t x, int32_t y) +{ + return Message::noinc(DP_msg_move_pointer_new(contextId, x, y)); +} + +Message makeMoveRegionMessage( + uint8_t contextId, uint16_t layer, int32_t bx, int32_t by, int32_t bw, + int32_t bh, int32_t x1, int32_t y1, int32_t x2, int32_t y2, int32_t x3, + int32_t y3, int32_t x4, int32_t y4, const QImage &mask) +{ + QByteArray compressed = + mask.isNull() ? QByteArray{} + : qCompress(mask.constBits(), mask.sizeInBytes()); + if(compressed.size() <= + DP_MESSAGE_MAX_PAYLOAD_LENGTH - DP_MSG_MOVE_REGION_STATIC_LENGTH) { + return Message::noinc(DP_msg_move_region_new( + contextId, layer, bx, by, bw, bh, x1, y1, x2, y2, x3, y3, x4, y4, + &Message::setUchars, compressed.size(), compressed.data())); + } else { + return Message::null(); + } +} + +static QByteArray compressAlphaMask(const QImage &mask) +{ + Q_ASSERT(mask.format() == QImage::Format_ARGB32_Premultiplied); + int width = mask.width(); + int height = mask.height(); + QByteArray alphaMask; + alphaMask.reserve(width * height); + for(int y = 0; y < height; ++y) { + const uchar *scanLine = mask.scanLine(y); + for(int x = 0; x < width; ++x) { + alphaMask.append(scanLine[x * 4 + 3]); + } + } + return qCompress(alphaMask); +} + +Message makeMoveRectMessage( + uint8_t contextId, uint16_t layer, uint16_t source, int32_t sx, int32_t sy, + int32_t tx, int32_t ty, int32_t w, int32_t h, const QImage &mask) +{ + QByteArray compressed = + mask.isNull() ? QByteArray{} : compressAlphaMask(mask); + if(compressed.size() <= + DP_MESSAGE_MAX_PAYLOAD_LENGTH - DP_MSG_MOVE_RECT_STATIC_LENGTH) { + return Message::noinc(DP_msg_move_rect_new( + contextId, layer, source, sx, sy, tx, ty, w, h, &Message::setUchars, + compressed.size(), compressed.data())); + } else { + return Message::null(); + } +} + +Message makeTransformRegionMessage( + uint8_t contextId, uint16_t layer, uint16_t source, int32_t bx, int32_t by, + int32_t bw, int32_t bh, int32_t x1, int32_t y1, int32_t x2, int32_t y2, + int32_t x3, int32_t y3, int32_t x4, int32_t y4, uint8_t mode, + const QImage &mask) +{ + QByteArray compressed = + mask.isNull() ? QByteArray{} : compressAlphaMask(mask); + if(compressed.size() <= + DP_MESSAGE_MAX_PAYLOAD_LENGTH - DP_MSG_TRANSFORM_REGION_STATIC_LENGTH) { + return Message::noinc(DP_msg_transform_region_new( + contextId, layer, source, bx, by, bw, bh, x1, y1, x2, y2, x3, y3, + x4, y4, mode, &Message::setUchars, compressed.size(), + compressed.data())); + } else { + return Message::null(); + } +} + +Message makePutImageMessage( + uint8_t contextId, uint16_t layer, uint8_t mode, uint32_t x, uint32_t y, + uint32_t w, uint32_t h, const QByteArray &compressedImage) +{ + return Message::noinc(DP_msg_put_image_new( + contextId, layer, mode, x, y, w, h, Message::setUchars, + compressedImage.size(), const_cast(compressedImage.data()))); +} + +Message +makeSetMetadataIntMessage(uint8_t contextId, uint8_t field, int32_t value) +{ + return Message::noinc(DP_msg_set_metadata_int_new(contextId, field, value)); +} + +Message makeTrackCreateMessage( + uint8_t contextId, uint16_t id, uint16_t insertId, uint16_t sourceId, + const QString &title) +{ + QByteArray bytes = title.toUtf8(); + return Message::noinc(DP_msg_track_create_new( + contextId, id, insertId, sourceId, bytes.constData(), bytes.length())); +} + +Message makeTrackDeleteMessage(uint8_t contextId, uint16_t id) +{ + return Message::noinc(DP_msg_track_delete_new(contextId, id)); +} + +Message +makeTrackOrderMessage(uint8_t contextId, const QVector &tracks) +{ + return Message::noinc(DP_msg_track_order_new( + contextId, Message::setUint16s, tracks.size(), + const_cast(tracks.constData()))); +} + +Message +makeTrackRetitleMessage(uint8_t contextId, uint16_t id, const QString &title) +{ + QByteArray bytes = title.toUtf8(); + return Message::noinc(DP_msg_track_retitle_new( + contextId, id, bytes.constData(), bytes.length())); +} + +Message makeUndoMessage(uint8_t contextId, uint8_t overrideUser, bool redo) +{ + return Message::noinc(DP_msg_undo_new(contextId, overrideUser, redo)); +} + +Message makeUndoDepthMessage(uint8_t contextId, uint8_t depth) +{ + return Message::noinc(DP_msg_undo_depth_new(contextId, depth)); +} + +Message makeUndoPointMessage(uint8_t contextId) +{ + return Message::noinc(DP_msg_undo_point_new(contextId)); +} + +Message makeUserAclMessage(uint8_t contextId, const QVector &users) +{ + return Message::noinc(DP_msg_user_acl_new( + contextId, Message::setUint8s, users.count(), + const_cast(users.constData()))); +} + +Message makeUserInfoMessage( + uint8_t contextId, uint8_t recipient, const QJsonDocument &msg) +{ + QByteArray msgBytes = msg.toJson(QJsonDocument::Compact); + return Message::noinc(DP_msg_data_new( + contextId, DP_MSG_DATA_TYPE_USER_INFO, recipient, Message::setUchars, + msgBytes.length(), const_cast(msgBytes.constData()))); +} + +static void makePutImagesRecursive( + MessageList &msgs, uint8_t contextId, uint16_t layer, uint8_t mode, int x, + int y, const QImage &image, const QRect &bounds, int estimatedSize) +{ + int w = bounds.width(); + int h = bounds.height(); + if(w > 0 && h > 0) { + int maxSize = 0xffff - 19; + int compressedSize; + // If our estimated size looks good, try compressing. Otherwise assume + // that the image is too big to fit into a message and split it up. + if(estimatedSize < maxSize) { + QImage subImage = + bounds == image.rect() ? image : image.copy(bounds); + QByteArray compressed = + qCompress(subImage.constBits(), subImage.sizeInBytes()); + compressedSize = compressed.size(); + if(compressedSize <= maxSize) { + msgs.append(makePutImageMessage( + contextId, layer, mode, x, y, w, h, compressed)); + return; + } + } else { + compressedSize = estimatedSize; + } + // (Probably) too big to fit in a message, slice in half along the + // longest axis. + int estimatedSliceSize = compressedSize / 2; + if(w > h) { + int sx1 = w / 2; + int sx2 = w - sx1; + makePutImagesRecursive( + msgs, contextId, layer, mode, x, y, image, + bounds.adjusted(0, 0, -sx1, 0), estimatedSliceSize); + makePutImagesRecursive( + msgs, contextId, layer, mode, x + sx2, y, image, + bounds.adjusted(sx2, 0, 0, 0), estimatedSliceSize); + } else { + int sy1 = h / 2; + int sy2 = h - sy1; + makePutImagesRecursive( + msgs, contextId, layer, mode, x, y, image, + bounds.adjusted(0, 0, 0, -sy1), estimatedSliceSize); + makePutImagesRecursive( + msgs, contextId, layer, mode, x, y + sy2, image, + bounds.adjusted(0, sy2, 0, 0), estimatedSliceSize); + } + } +} + +void makePutImageMessages( + MessageList &msgs, uint8_t contextId, uint16_t layer, uint8_t mode, int x, + int y, const QImage &image) +{ + // If the image is totally outside of the canvas, there's nothing to put. + if(x >= -image.width() && y >= -image.height()) { + QImage converted = + image.convertToFormat(QImage::Format_ARGB32_Premultiplied); + if(x < 0 || y < 0) { + // Crop image, since the protocol doesn't do negative coordinates. + int xoffset = x < 0 ? -x : 0; + int yoffset = y < 0 ? -y : 0; + QImage cropped = converted.copy( + xoffset, yoffset, image.width() - xoffset, + image.height() - yoffset); + makePutImagesRecursive( + msgs, contextId, layer, mode, x + xoffset, y + yoffset, cropped, + cropped.rect(), 0); + } else { + makePutImagesRecursive( + msgs, contextId, layer, mode, x, y, converted, converted.rect(), + 0); + } + } +} + +Message makeLocalChangeLayerVisibilityMessage(int layerId, bool hidden) +{ + return Message::noinc( + DP_local_state_msg_layer_visibility_new(layerId, hidden)); +} + +Message makeLocalChangeBackgroundColorMessage(const QColor &color) +{ + drawdance::DrawContext drawContext = drawdance::DrawContextPool::acquire(); + drawdance::Tile tile = drawdance::Tile::fromColor(color); + return Message::noinc( + DP_local_state_msg_background_tile_new(drawContext.get(), tile.get())); +} + +Message makeLocalChangeBackgroundClearMessage() +{ + return Message::noinc( + DP_local_state_msg_background_tile_new(nullptr, nullptr)); +} + + +Message makeLocalChangeViewModeMessage(DP_ViewMode viewMode) +{ + return Message::noinc(DP_local_state_msg_view_mode_new(viewMode)); +} + +Message makeLocalChangeActiveLayerMessage(int layerId) +{ + return Message::noinc(DP_local_state_msg_active_layer_new(layerId)); +} + +Message makeLocalChangeActiveFrameMessage(int frameIndex) +{ + return Message::noinc(DP_local_state_msg_active_frame_new(frameIndex)); +} + +Message makeLocalChangeOnionSkinsMessage(const DP_OnionSkins *oss) +{ + return Message::noinc(DP_local_state_msg_onion_skins_new(oss)); +} + +Message makeLocalChangeTrackVisibilityMessage(int trackId, bool hidden) +{ + return Message::noinc( + DP_local_state_msg_track_visibility_new(trackId, hidden)); +} + +Message makeLocalChangeTrackOnionSkinMessage(int trackId, bool onionSkin) +{ + return Message::noinc( + DP_local_state_msg_track_onion_skin_new(trackId, onionSkin)); +} + +static void setClassicDabs(int count, DP_ClassicDab *out, void *user) +{ + const DP_ClassicDab *cds = static_cast(user); + for(int i = 0; i < count; ++i) { + const DP_ClassicDab *cd = DP_classic_dab_at(cds, i); + DP_classic_dab_init( + out, i, DP_classic_dab_x(cd), DP_classic_dab_y(cd), + DP_classic_dab_size(cd), DP_classic_dab_hardness(cd), + DP_classic_dab_opacity(cd)); + } +} + +static void setPixelDabs(int count, DP_PixelDab *out, void *user) +{ + const DP_PixelDab *pds = static_cast(user); + for(int i = 0; i < count; ++i) { + const DP_PixelDab *pd = DP_pixel_dab_at(pds, i); + DP_pixel_dab_init( + out, i, DP_pixel_dab_x(pd), DP_pixel_dab_y(pd), + DP_pixel_dab_size(pd), DP_pixel_dab_opacity(pd)); + } +} + +Message makeMessageBackwardCompatible(const Message &msg) +{ + DP_MessageType type = msg.type(); + switch(type) { + case DP_MSG_SERVER_COMMAND: + case DP_MSG_DISCONNECT: + case DP_MSG_PING: + case DP_MSG_INTERNAL: + case DP_MSG_JOIN: + case DP_MSG_LEAVE: + case DP_MSG_SESSION_OWNER: + case DP_MSG_CHAT: + case DP_MSG_TRUSTED_USERS: + case DP_MSG_SOFT_RESET: + case DP_MSG_PRIVATE_CHAT: + case DP_MSG_INTERVAL: + case DP_MSG_LASER_TRAIL: + case DP_MSG_MOVE_POINTER: + case DP_MSG_MARKER: + case DP_MSG_LAYER_ACL: + case DP_MSG_DEFAULT_LAYER: + case DP_MSG_FILTERED: + case DP_MSG_EXTENSION: + case DP_MSG_UNDO_POINT: + case DP_MSG_CANVAS_RESIZE: + case DP_MSG_LAYER_CREATE: + case DP_MSG_LAYER_RETITLE: + case DP_MSG_LAYER_ORDER: + case DP_MSG_LAYER_DELETE: + case DP_MSG_PEN_UP: + case DP_MSG_ANNOTATION_CREATE: + case DP_MSG_ANNOTATION_RESHAPE: + case DP_MSG_ANNOTATION_EDIT: + case DP_MSG_ANNOTATION_DELETE: + case DP_MSG_MOVE_REGION: + case DP_MSG_PUT_TILE: + case DP_MSG_CANVAS_BACKGROUND: + case DP_MSG_UNDO: + return msg; + case DP_MSG_USER_ACL: { + DP_MsgUserAcl *mua = msg.toUserAcl(); + int count; + const uint8_t *users = DP_msg_user_acl_users(mua, &count); + QVector compatibleUsers; + compatibleUsers.reserve(count); + for(int i = 0; i < count; ++i) { + uint8_t user = users[i]; + if(user != 0) { // Reset locks aren't in Drawpile 2.1. + compatibleUsers.append(user); + } + } + int compatibleCount = compatibleUsers.size(); + if(compatibleCount == count) { + return msg; + } else { + qDebug( + "Making %s message compatible", qUtf8Printable(msg.typeName())); + return Message::noinc(DP_msg_feature_access_levels_new( + msg.contextId(), Message::setUint8s, compatibleCount, + compatibleUsers.data())); + } + } + case DP_MSG_FEATURE_ACCESS_LEVELS: { + DP_MsgFeatureAccessLevels *mfal = msg.toFeatureAccessLevels(); + int count; + const uint8_t *tiers = + DP_msg_feature_access_levels_feature_tiers(mfal, &count); + if(count == 9) { + return msg; + } else { + qDebug( + "Making %s message compatible", qUtf8Printable(msg.typeName())); + uint8_t compatibleTiers[9]; + for(int i = 0; i < 9; ++i) { + compatibleTiers[i] = i < count ? tiers[i] : 0; + } + return Message::noinc(DP_msg_feature_access_levels_new( + msg.contextId(), Message::setUint8s, 9, compatibleTiers)); + } + } + case DP_MSG_LAYER_ATTRIBUTES: { + DP_MsgLayerAttributes *mla = msg.toLayerAttributes(); + bool compatible = canvas::blendmode::isBackwardCompatibleMode( + DP_BlendMode(DP_msg_layer_attributes_blend(mla))); + if(compatible) { + return msg; + } else { + qDebug( + "Making %s message compatible", qUtf8Printable(msg.typeName())); + return Message::noinc(DP_msg_layer_attributes_new( + msg.contextId(), DP_msg_layer_attributes_id(mla), + DP_msg_layer_attributes_sublayer(mla), + DP_msg_layer_attributes_flags(mla), + DP_msg_layer_attributes_opacity(mla), DP_BLEND_MODE_NORMAL)); + } + } + case DP_MSG_PUT_IMAGE: { + DP_MsgPutImage *mpi = msg.toPutImage(); + bool compatible = canvas::blendmode::isBackwardCompatibleMode( + DP_BlendMode(DP_msg_put_image_mode(mpi))); + if(compatible) { + return msg; + } else { + qDebug( + "Making %s message compatible", qUtf8Printable(msg.typeName())); + size_t size; + const unsigned char *image = DP_msg_put_image_image(mpi, &size); + return Message::noinc(DP_msg_put_image_new( + msg.contextId(), DP_msg_put_image_layer(mpi), + DP_BLEND_MODE_NORMAL, DP_msg_put_image_x(mpi), + DP_msg_put_image_y(mpi), DP_msg_put_image_w(mpi), + DP_msg_put_image_h(mpi), Message::setUchars, size, + const_cast(image))); + } + } + case DP_MSG_FILL_RECT: { + DP_MsgFillRect *mfr = msg.toFillRect(); + bool compatible = canvas::blendmode::isBackwardCompatibleMode( + DP_BlendMode(DP_msg_fill_rect_mode(mfr))); + if(compatible) { + return msg; + } else { + qDebug( + "Making %s message compatible", qUtf8Printable(msg.typeName())); + return Message::noinc(DP_msg_fill_rect_new( + msg.contextId(), DP_msg_fill_rect_layer(mfr), + DP_BLEND_MODE_NORMAL, DP_msg_fill_rect_x(mfr), + DP_msg_fill_rect_y(mfr), DP_msg_fill_rect_w(mfr), + DP_msg_fill_rect_h(mfr), DP_msg_fill_rect_color(mfr))); + } + } + case DP_MSG_DRAW_DABS_CLASSIC: { + DP_MsgDrawDabsClassic *mddc = msg.toDrawDabsClassic(); + ; + bool compatible = canvas::blendmode::isBackwardCompatibleMode( + DP_BlendMode(DP_msg_draw_dabs_classic_mode(mddc))); + if(compatible) { + return msg; + } else { + qDebug( + "Making %s message compatible", qUtf8Printable(msg.typeName())); + int count; + const DP_ClassicDab *cds = + DP_msg_draw_dabs_classic_dabs(mddc, &count); + return Message::noinc(DP_msg_draw_dabs_classic_new( + msg.contextId(), DP_msg_draw_dabs_classic_layer(mddc), + DP_msg_draw_dabs_classic_x(mddc), + DP_msg_draw_dabs_classic_y(mddc), + DP_msg_draw_dabs_classic_color(mddc), DP_BLEND_MODE_NORMAL, + setClassicDabs, count, const_cast(cds))); + } + } + case DP_MSG_DRAW_DABS_PIXEL: + case DP_MSG_DRAW_DABS_PIXEL_SQUARE: { + DP_MsgDrawDabsPixel *mddp = msg.toDrawDabsPixel(); + bool compatible = canvas::blendmode::isBackwardCompatibleMode( + DP_BlendMode(DP_msg_draw_dabs_pixel_mode(mddp))); + if(compatible) { + return msg; + } else { + qDebug( + "Making %s message compatible", qUtf8Printable(msg.typeName())); + int count; + const DP_PixelDab *pds = DP_msg_draw_dabs_pixel_dabs(mddp, &count); + return Message::noinc(( + type == DP_MSG_DRAW_DABS_PIXEL + ? DP_msg_draw_dabs_pixel_new + : DP_msg_draw_dabs_pixel_square_new)( + msg.contextId(), DP_msg_draw_dabs_pixel_layer(mddp), + DP_msg_draw_dabs_pixel_x(mddp), DP_msg_draw_dabs_pixel_y(mddp), + DP_msg_draw_dabs_pixel_color(mddp), DP_BLEND_MODE_NORMAL, + setPixelDabs, count, const_cast(pds))); + } + } + case DP_MSG_LAYER_TREE_CREATE: { + DP_MsgLayerTreeCreate *mltc = msg.toLayerTreeCreate(); + uint8_t flags = DP_msg_layer_tree_create_flags(mltc); + uint16_t sourceId = DP_msg_layer_tree_create_source(mltc); + uint16_t targetId = DP_msg_layer_tree_create_target(mltc); + bool involvesGroups = (flags & DP_MSG_LAYER_TREE_CREATE_FLAGS_GROUP) || + (flags & DP_MSG_LAYER_TREE_CREATE_FLAGS_INTO); + bool sourceAndTargetDiffer = + sourceId != 0 && targetId != 0 && sourceId != targetId; + if(involvesGroups || sourceAndTargetDiffer) { + return Message::null(); + } else { + qDebug( + "Making %s message compatible", qUtf8Printable(msg.typeName())); + uint8_t compatFlags = + (sourceId == 0 ? 0 : DP_MSG_LAYER_CREATE_FLAGS_COPY) | + (targetId == 0 ? 0 : DP_MSG_LAYER_CREATE_FLAGS_INSERT); + uint16_t compatSourceId = sourceId == 0 ? targetId : sourceId; + size_t titleLength; + const char *title = + DP_msg_layer_tree_create_title(mltc, &titleLength); + return Message::noinc(DP_msg_layer_create_new( + msg.contextId(), DP_msg_layer_tree_create_id(mltc), + compatSourceId, DP_msg_layer_tree_create_fill(mltc), + compatFlags, title, titleLength)); + } + } + default: + return Message::null(); + } +} +} diff --git a/src/libclient/net/message.h b/src/libclient/net/message.h new file mode 100644 index 0000000000..eaa44d5aca --- /dev/null +++ b/src/libclient/net/message.h @@ -0,0 +1,171 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#ifndef LIBCLIENT_DRAWDANCE_MESSAGE_H +#define LIBCLIENT_DRAWDANCE_MESSAGE_H +extern "C" { +#include +#include +} +#include +#include + +class QByteArray; +class QColor; +class QImage; +class QJsonDocument; +class QString; +struct DP_OnionSkins; + +namespace net { + +Message makeAnnotationCreateMessage( + unsigned int contextId, uint16_t id, int32_t x, int32_t y, uint16_t w, + uint16_t h); + +Message makeAnnotationDeleteMessage(uint8_t contextId, uint16_t id); + +Message makeAnnotationEditMessage( + uint8_t contextId, uint16_t id, uint32_t bg, uint8_t flags, uint8_t border, + const QString &text); + +Message makeAnnotationReshapeMessage( + uint8_t contextId, uint16_t id, int32_t x, int32_t y, uint16_t w, + uint16_t h); + +Message makeCanvasBackgroundMessage(uint8_t contextId, const QColor &color); + +Message makeCanvasResizeMessage( + uint8_t contextId, int32_t top, int32_t right, int32_t bottom, + int32_t left); + +Message makeDefaultLayerMessage(uint8_t contextId, uint16_t id); + +Message makeFeatureAccessLevelsMessage( + uint8_t contextId, int featureCount, const uint8_t *features); + +Message makeFillRectMessage( + uint8_t contextId, uint16_t layer, uint8_t mode, uint32_t x, uint32_t y, + uint32_t w, uint32_t h, const QColor &color); + +Message makeInternalCatchupMessage(uint8_t contextId, int progress); + +Message makeInternalCleanupMessage(uint8_t contextId); + +Message makeInternalResetMessage(uint8_t contextId); + +Message makeInternalSnapshotMessage(uint8_t contextId); + +Message makeKeyFrameSetMessage( + uint8_t contextId, uint16_t trackId, uint16_t frameIndex, uint16_t sourceId, + uint16_t sourceIndex, uint8_t source); + +Message makeKeyFrameLayerAttributesMessage( + uint8_t contextId, uint16_t trackId, uint16_t frameIndex, + const QVector &layers); + +Message makeKeyFrameRetitleMessage( + uint8_t contextId, uint16_t trackId, uint16_t frameIndex, + const QString &title); + +Message makeKeyFrameDeleteMessage( + uint8_t contextId, uint16_t trackId, uint16_t frameIndex, + uint16_t moveTrackId, uint16_t moveFrameIndex); + +Message +makeLaserTrailMessage(uint8_t contextId, uint32_t color, uint8_t persistence); + +Message makeLayerAttributesMessage( + uint8_t contextId, uint16_t id, uint8_t sublayer, uint8_t flags, + uint8_t opacity, uint8_t blend); + +Message makeLayerAclMessage( + uint8_t contextId, uint16_t id, uint8_t flags, + const QVector &exclusive); + +Message makeLayerCreateMessage( + uint8_t contextId, uint16_t id, uint16_t source, uint32_t fill, + uint8_t flags, const QString &name); + +Message makeLayerTreeCreateMessage( + uint8_t contextId, uint16_t id, uint16_t source, uint16_t target, + uint32_t fill, uint8_t flags, const QString &name); + +Message makeLayerDeleteMessage(uint8_t contextId, uint16_t id, bool merge); + +Message +makeLayerTreeDeleteMessage(uint8_t contextId, uint16_t id, uint16_t mergeTo); + +Message makeLayerTreeMoveMessage( + uint8_t contextId, uint16_t layer, uint16_t parent, uint16_t sibling); + +Message +makeLayerRetitleMessage(uint8_t contextId, uint16_t id, const QString &title); + +Message makeMovePointerMessage(uint8_t contextId, int32_t x, int32_t y); + +Message makeMoveRegionMessage( + uint8_t contextId, uint16_t layer, int32_t bx, int32_t by, int32_t bw, + int32_t bh, int32_t x1, int32_t y1, int32_t x2, int32_t y2, int32_t x3, + int32_t y3, int32_t x4, int32_t y4, const QImage &mask); + +Message makeMoveRectMessage( + uint8_t contextId, uint16_t layer, uint16_t source, int32_t sx, int32_t sy, + int32_t tx, int32_t ty, int32_t w, int32_t h, const QImage &mask); + +Message makeTransformRegionMessage( + uint8_t contextId, uint16_t layer, uint16_t source, int32_t bx, int32_t by, + int32_t bw, int32_t bh, int32_t x1, int32_t y1, int32_t x2, int32_t y2, + int32_t x3, int32_t y3, int32_t x4, int32_t y4, uint8_t mode, + const QImage &mask); + +Message makePutImageMessage( + uint8_t contextId, uint16_t layer, uint8_t mode, uint32_t x, uint32_t y, + uint32_t w, uint32_t h, const QByteArray &compressedImage); + +Message +makeSetMetadataIntMessage(uint8_t contextId, uint8_t field, int32_t value); + +Message makeTrackCreateMessage( + uint8_t contextId, uint16_t id, uint16_t insertId, uint16_t sourceId, + const QString &title); + +Message makeTrackDeleteMessage(uint8_t contextId, uint16_t id); + +Message +makeTrackOrderMessage(uint8_t contextId, const QVector &tracks); + +Message +makeTrackRetitleMessage(uint8_t contextId, uint16_t id, const QString &title); + +Message makeUndoMessage(uint8_t contextId, uint8_t overrideUser, bool redo); + +Message makeUndoDepthMessage(uint8_t contextId, uint8_t depth); + +Message makeUndoPointMessage(uint8_t contextId); + +Message makeUserAclMessage(uint8_t contextId, const QVector &users); + +Message makeUserInfoMessage( + uint8_t contextId, uint8_t recipient, const QJsonDocument &msg); + +// Fills given message list with put image messages, potentially cropping +// the image if the given coordinates are negative and splitting it into +// multiple messages if it doesn't fit into a single one. +void makePutImageMessages( + MessageList &msgs, uint8_t contextId, uint16_t layer, uint8_t mode, int x, + int y, const QImage &image); + +Message makeLocalChangeLayerVisibilityMessage(int layerId, bool hidden); +Message makeLocalChangeBackgroundColorMessage(const QColor &color); +Message makeLocalChangeBackgroundClearMessage(); +Message makeLocalChangeViewModeMessage(DP_ViewMode viewMode); +Message makeLocalChangeActiveLayerMessage(int layerId); +Message makeLocalChangeActiveFrameMessage(int frameIndex); +Message makeLocalChangeOnionSkinsMessage(const DP_OnionSkins *oss); +Message makeLocalChangeTrackVisibilityMessage(int trackId, bool hidden); +Message makeLocalChangeTrackOnionSkinMessage(int trackId, bool onionSkin); + +Message makeMessageBackwardCompatible(const Message &msg); + +} + +#endif diff --git a/src/libclient/net/messagequeue.cpp b/src/libclient/net/messagequeue.cpp deleted file mode 100644 index 938d7df03a..0000000000 --- a/src/libclient/net/messagequeue.cpp +++ /dev/null @@ -1,517 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -#include "libclient/net/messagequeue.h" -#include "libclient/settings.h" -#include "libshared/util/qtcompat.h" - -#include -#include -#include -#include -#include - -namespace net { - -// Reserve enough buffer space for one complete message -static const int MAX_BUF_LEN = 0xffff + DP_MESSAGE_HEADER_LENGTH; - -// Special message types handled internally by this class -static const int MSG_TYPE_DISCONNECT = 1; -static const int MSG_TYPE_PING = 2; - -MessageQueue::MessageQueue(QTcpSocket *socket, QObject *parent) - : QObject(parent), m_socket(socket), - m_smoothEnabled(false), - m_smoothDrainRate(libclient::settings::defaultMessageQueueDrainRate), - m_smoothTimer(nullptr), - m_smoothMessagesToDrain(INT_MAX), - m_contextId(0), - m_pingTimer(nullptr), - m_lastRecvTime(0), - m_idleTimeout(0), m_pingSent(0), - m_gracefullyDisconnecting(false), - m_artificialLagMs(0), - m_artificialLagTimer(nullptr) -{ - connect(socket, &QTcpSocket::readyRead, this, &MessageQueue::readData); - connect(socket, &QTcpSocket::bytesWritten, this, &MessageQueue::dataWritten); - - if(socket->inherits("QSslSocket")) { - connect(socket, SIGNAL(encrypted()), this, SLOT(sslEncrypted())); - } - - m_recvbuffer = new char[MAX_BUF_LEN]; - m_recvbytes = 0; - m_sentbytes = 0; - - m_idleTimer = new QTimer(this); - m_idleTimer->setTimerType(Qt::CoarseTimer); - connect(m_idleTimer, &QTimer::timeout, this, &MessageQueue::checkIdleTimeout); - m_idleTimer->setInterval(1000); - m_idleTimer->setSingleShot(false); -} - -void MessageQueue::sslEncrypted() -{ - disconnect(m_socket, &QTcpSocket::bytesWritten, this, &MessageQueue::dataWritten); - connect(m_socket, SIGNAL(encryptedBytesWritten(qint64)), this, SLOT(dataWritten(qint64))); -} - -void MessageQueue::checkIdleTimeout() -{ - if(m_idleTimeout>0 && m_socket->state() == QTcpSocket::ConnectedState && idleTime() > m_idleTimeout) { - qWarning("MessageQueue timeout"); - m_socket->abort(); - } -} - -void MessageQueue::setIdleTimeout(qint64 timeout) -{ - m_idleTimeout = timeout; - m_lastRecvTime = QDateTime::currentMSecsSinceEpoch(); - if(timeout>0) - m_idleTimer->start(1000); - else - m_idleTimer->stop(); -} - -void MessageQueue::setPingInterval(int msecs) -{ - if(!m_pingTimer) { - m_pingTimer = new QTimer(this); - m_pingTimer->setTimerType(Qt::CoarseTimer); - m_pingTimer->setSingleShot(false); - connect(m_pingTimer, &QTimer::timeout, this, &MessageQueue::sendPing); - } - m_pingTimer->setInterval(msecs); - m_pingTimer->start(msecs); -} - -void MessageQueue::setSmoothEnabled(bool enabled) -{ - m_smoothEnabled = enabled; - updateSmoothing(); -} - -void MessageQueue::setSmoothDrainRate(int smoothDrainRate) -{ - m_smoothDrainRate = qBound( - 0, smoothDrainRate, libclient::settings::maxMessageQueueDrainRate); - updateSmoothing(); -} - -void MessageQueue::updateSmoothing() -{ - bool enabled = m_smoothEnabled && m_smoothDrainRate > 0; - if(enabled && !m_smoothTimer) { - m_smoothTimer = new QTimer{this}; - m_smoothTimer->setTimerType(Qt::PreciseTimer); - m_smoothTimer->setSingleShot(false); - m_smoothTimer->setInterval(SMOOTHING_INTERVAL_MSEC); - connect( - m_smoothTimer, &QTimer::timeout, this, - &MessageQueue::receiveSmoothedMessages); - } else if(!enabled && m_smoothTimer) { - delete m_smoothTimer; - m_smoothTimer = nullptr; - if(!m_smoothBuffer.isEmpty()) { - m_inbox.append(m_smoothBuffer); - m_smoothBuffer.clear(); - emit messageAvailable(); - } - } -} - -void MessageQueue::setArtificialLagMs(int msecs) -{ - m_artificialLagMs = qMax(0, msecs); - if(m_artificialLagMs != 0 && !m_artificialLagTimer) { - m_artificialLagTimer = new QTimer(this); - m_artificialLagTimer->setTimerType(Qt::PreciseTimer); - m_artificialLagTimer->setSingleShot(true); - connect(m_artificialLagTimer, &QTimer::timeout, this, - &MessageQueue::sendArtificallyLaggedMessages); - } -} - -MessageQueue::~MessageQueue() -{ - delete [] m_recvbuffer; -} - -bool MessageQueue::isPending() const -{ - return !m_inbox.isEmpty(); -} - -void MessageQueue::receive(drawdance::MessageList &buffer) -{ - buffer.swap(m_inbox); -} - -void MessageQueue::send(const drawdance::Message &msg) -{ - sendMultiple(1, &msg); -} - -void MessageQueue::sendMultiple(int count, const drawdance::Message *msgs) -{ - if(m_artificialLagMs == 0) { - enqueueMessages(count, msgs); - } else { - long long time = - QDateTime::currentMSecsSinceEpoch() + m_artificialLagMs; - for(int i = 0; i < count; ++i) { - m_artificialLagTimes.append(time); - m_artificialLagMessages.append(msgs[i]); - } - if(!m_artificialLagTimer->isActive()) { - m_artificialLagTimer->start(m_artificialLagMs); - } - } -} - -void MessageQueue::receiveSmoothedMessages() -{ - int count = m_smoothBuffer.size(); - bool smoothTimerActive = m_smoothTimer->isActive(); - if(count == 0) { - if(smoothTimerActive) { - m_smoothTimer->stop(); - } - } else { - if(m_smoothMessagesToDrain > count) { - m_inbox.append(m_smoothBuffer); - m_smoothBuffer.clear(); - } else { - // We only smoothe strokes, all other messages get flushed along. - int drainCount = 0; - int drainBuffer = 0; - do { - drawdance::Message msg = m_smoothBuffer.takeFirst(); - if(msg.shouldSmoothe()) { - drainCount += drainBuffer + 1; - drainBuffer = 0; - } else { - ++drainBuffer; - } - m_inbox.append(msg); - } while(drainCount < m_smoothMessagesToDrain && !m_smoothBuffer.isEmpty()); - } - - if(!m_smoothBuffer.isEmpty() && !smoothTimerActive) { - m_smoothTimer->start(); - } - emit messageAvailable(); - } -} - -void MessageQueue::sendArtificallyLaggedMessages() -{ - long long now = QDateTime::currentMSecsSinceEpoch(); - int count = m_artificialLagTimes.count(); - int i = 0; - while(i < count) { - long long time = m_artificialLagTimes[i]; - if(time <= now) { - ++i; - } else { - break; - } - } - - enqueueMessages(i, m_artificialLagMessages.constData()); - m_artificialLagTimes.remove(0, i); - m_artificialLagMessages.remove(0, i); - - if(i < count) { - m_artificialLagTimer->start(m_artificialLagTimes.first() - now); - } -} - -void MessageQueue::enqueueMessages(int count, const drawdance::Message *msgs) -{ - if(!m_gracefullyDisconnecting) { - for(int i = 0; i < count; ++i) { - m_outbox.enqueue(msgs[i]); - } - if(m_sendbuffer.isEmpty()) { - writeData(); - } - } -} - -void MessageQueue::sendPingMsg(bool pong) -{ - if(m_artificialLagMs == 0) { - m_pings.enqueue(pong); - if(m_sendbuffer.isEmpty()) { - writeData(); - } - } else { - // Not accurate, but probably good enough for development purposes. - send(drawdance::Message::makePing(0, pong)); - } -} - -void MessageQueue::sendDisconnect(GracefulDisconnect reason, const QString &message) -{ - if(m_gracefullyDisconnecting) - qWarning("sendDisconnect: already disconnecting."); - - drawdance::Message msg = - drawdance::Message::makeDisconnect(0, uint8_t(reason), message); - - qInfo("Sending disconnect message (reason=%d), will disconnect after queue (%lld messages) is empty.", int(reason), compat::cast(m_outbox.size() + m_pings.size())); - send(msg); - m_gracefullyDisconnecting = true; - m_recvbytes = 0; - setSmoothEnabled(false); -} - -void MessageQueue::sendPing() -{ - if(m_pingSent==0) { - m_pingSent = QDateTime::currentMSecsSinceEpoch(); - - } else { - // This can happen if the other side's upload buffer is too full - // for the Pong to make it through in time. - qWarning("sendPing(): reply to previous ping not yet received!"); - } - - sendPingMsg(false); - -} - -int MessageQueue::uploadQueueBytes() const -{ - int total = m_socket->bytesToWrite() + m_sendbuffer.length() - m_sentbytes; - for(const drawdance::Message &msg : m_outbox) - total += compat::castSize(msg.length()); - total += m_pings.size() * (DP_MESSAGE_HEADER_LENGTH + DP_MSG_PING_STATIC_LENGTH); - return total; -} - -bool MessageQueue::isUploading() const -{ - return !m_sendbuffer.isEmpty() || m_socket->bytesToWrite() > 0; -} - -qint64 MessageQueue::idleTime() const -{ - return QDateTime::currentMSecsSinceEpoch() - m_lastRecvTime; -} - -int MessageQueue::haveWholeMessageToRead() -{ - if(m_recvbytes>= DP_MESSAGE_HEADER_LENGTH) { - int bodyLength = qFromBigEndian(m_recvbuffer); - int messageLength = bodyLength + DP_MESSAGE_HEADER_LENGTH; - if(m_recvbytes >= messageLength) { - return messageLength; - } - } - return 0; -} - -void MessageQueue::readData() { - int read, totalread = 0, gotmessages = 0; - bool smoothFlush = false; - do { - // Read as much as fits in to the message buffer - read = m_socket->read(m_recvbuffer+m_recvbytes, MAX_BUF_LEN-m_recvbytes); - if(read<0) { - emit socketError(m_socket->errorString()); - return; - } - - if(m_gracefullyDisconnecting) { - // Ignore incoming data when we're in the process of disconnecting - if(read>0) - continue; - else - return; - } - - m_recvbytes += read; - - // Extract all complete messages - int messageLength; - while((messageLength = haveWholeMessageToRead()) != 0) { - // Whole message received! - int type = static_cast(m_recvbuffer[2]); - if(type == MSG_TYPE_PING) { - // Pings are handled internally - if(messageLength != DP_MESSAGE_HEADER_LENGTH + 1) { - // Not a valid Ping message! - emit badData(messageLength, MSG_TYPE_PING, 0); - } else { - handlePing(m_recvbuffer[DP_MESSAGE_HEADER_LENGTH]); - } - - } else if(type == MSG_TYPE_DISCONNECT) { - // Graceful disconnects are also handled internally - if(messageLength < DP_MESSAGE_HEADER_LENGTH + 1) { - // We expected at least a reason! - emit badData(messageLength, MSG_TYPE_DISCONNECT, 0); - } else { - emit gracefulDisconnect( - GracefulDisconnect(m_recvbuffer[DP_MESSAGE_HEADER_LENGTH]), - QString::fromUtf8 - (m_recvbuffer + DP_MESSAGE_HEADER_LENGTH + 1, - messageLength - DP_MESSAGE_HEADER_LENGTH - 1)); - } - - } else { - // The rest are normal messages - drawdance::Message msg = drawdance::Message::deserialize( - reinterpret_cast(m_recvbuffer), m_recvbytes); - if(msg.isNull()) { - qWarning("Error deserializing message: %s", DP_error()); - emit badData(messageLength, type, static_cast(m_recvbuffer[3])); - } else { - if(m_smoothTimer) { - // Undos already have a delay because they require a - // round trip, we don't want to make them even slower. - bool ownUndoReceived = m_contextId != 0 && - msg.type() == DP_MSG_UNDO && - msg.contextId() == m_contextId; - if(ownUndoReceived) { - smoothFlush = true; - } - m_smoothBuffer.append(msg); - } else { - m_inbox.append(msg); - } - ++gotmessages; - } - } - - if(messageLength < m_recvbytes) { - // Buffer contains more than one message - memmove(m_recvbuffer, m_recvbuffer+messageLength, m_recvbytes-messageLength); - } - - m_recvbytes -= messageLength; - } - - // All whole messages extracted from the work buffer. - // There can still be more bytes in the socket buffer. - totalread += read; - } while(read>0); - - if(totalread) { - m_lastRecvTime = QDateTime::currentMSecsSinceEpoch(); - emit bytesReceived(totalread); - } - - if(gotmessages != 0) { - if(m_smoothTimer) { - if(smoothFlush) { - m_inbox.append(m_smoothBuffer); - m_smoothBuffer.clear(); - emit messageAvailable(); - m_smoothTimer->stop(); - } else { - m_smoothMessagesToDrain = - m_smoothBuffer.size() / m_smoothDrainRate; - if(!m_smoothTimer->isActive()) { - receiveSmoothedMessages(); - } - } - } else { - emit messageAvailable(); - } - } -} - -void MessageQueue::handlePing(bool isPong) -{ - if(isPong) { - // We got a Pong back: measure latency - if(m_pingSent==0) { - // Lots of pings can have been queued up - qDebug("Received Pong, but no Ping was sent!"); - - } else { - qint64 roundtrip = QDateTime::currentMSecsSinceEpoch() - m_pingSent; - m_pingSent = 0; - emit pingPong(roundtrip); - } - } else { - // Reply to a Ping with a Pong - sendPingMsg(true); - } -} - -void MessageQueue::dataWritten(qint64 bytes) -{ - emit bytesSent(bytes); - - // Write more once the buffer is empty - if(m_socket->bytesToWrite()==0) { - if(m_sendbuffer.isEmpty() && !messagesInOutbox() && m_gracefullyDisconnecting) { - qInfo("All sent, gracefully disconnecting."); - m_socket->disconnectFromHost(); - - } else { - writeData(); - } - } -} - -void MessageQueue::writeData() { - bool sendMore = true; - int sentBatch = 0; - - while(sendMore && sentBatch < 1024*64) { - sendMore = false; - if(m_sendbuffer.isEmpty() && messagesInOutbox()) { - // Upload buffer is empty, but there are messages in the outbox - Q_ASSERT(m_sentbytes == 0); - if(!dequeueFromOutbox().serialize(m_sendbuffer)) { - qWarning("Error serializing message: %s", DP_error()); - sendMore = messagesInOutbox(); - continue; - } - } - - if(m_sentbytes < m_sendbuffer.length()) { - const int sent = m_socket->write(m_sendbuffer.constData()+m_sentbytes, m_sendbuffer.length() - m_sentbytes); - if(sent<0) { - // Error - emit socketError(m_socket->errorString()); - return; - } - m_sentbytes += sent; - sentBatch += sent; - - Q_ASSERT(m_sentbytes <= m_sendbuffer.length()); - - if(m_sentbytes >= m_sendbuffer.length()) { - // Complete envelope sent - m_sendbuffer.clear(); - m_sentbytes = 0; - sendMore = messagesInOutbox(); - } - } - } -} - -bool MessageQueue::messagesInOutbox() const -{ - return !m_outbox.isEmpty() || !m_pings.isEmpty(); -} - -drawdance::Message MessageQueue::dequeueFromOutbox() -{ - if(m_pings.isEmpty()) { - return m_outbox.dequeue(); - } else { - return drawdance::Message::makePing(0, m_pings.dequeue()); - } -} - -} - diff --git a/src/libclient/net/messagequeue.h b/src/libclient/net/messagequeue.h deleted file mode 100644 index a15328a427..0000000000 --- a/src/libclient/net/messagequeue.h +++ /dev/null @@ -1,231 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -#ifndef DP_CLIENT_MSGQUEUE_H -#define DP_CLIENT_MSGQUEUE_H - -#include "libclient/drawdance/message.h" - -#include -#include -#include - -class QTcpSocket; -class QTimer; - -namespace net { - -/** - * A wrapper for an IO device for sending and receiving messages. - */ -class MessageQueue final : public QObject { -Q_OBJECT -public: - enum class GracefulDisconnect { - Error, // An error occurred - Kick, // client was kicked by the session operator - Shutdown, // server is shutting down - Other, // other unspecified error - }; - - /** - * @brief Create a message queue that wraps a TCP socket. - * - * The MessageQueue does not take ownership of the device. - */ - explicit MessageQueue(QTcpSocket *socket, QObject *parent=nullptr); - ~MessageQueue() override; - - /** - * @brief Check if there are new messages available - * @return true if getPending will return a message - */ - bool isPending() const; - - /** - * Get received messages, swaps (!) the given buffer with the inbox. - */ - void receive(drawdance::MessageList &buffer); - - /** - * Enqueue a single message for sending. - */ - void send(const drawdance::Message &msg); - - /** - * Enqueue multiple messages for sending. - */ - void sendMultiple(int count, const drawdance::Message *msgs); - - /** - * @brief Gracefully disconnect - * - * This function enqueues the disconnect notification message. The connection will - * be automatically closed after the message has been sent. Additionally, it - * causes all incoming messages to be ignored and no more data to be accepted - * for sending. - * - * @param reason - * @param message - */ - void sendDisconnect(GracefulDisconnect reason, const QString &message); - - /** - * @brief Get the number of bytes in the upload queue - * @return - */ - int uploadQueueBytes() const; - - /** - * @brief Is there still data in the upload buffer? - */ - bool isUploading() const; - - /** - * @brief Get the number of milliseconds since the last message sent by the remote end - */ - qint64 idleTime() const; - - /** - * @brief Set the maximum time the remote end can be quiet before timing out - * - * This can be used together with a keepalive message to detect disconnects more - * reliably than relying on TCP, which may have a very long timeout. - * - * @param timeout timeout in milliseconds - */ - void setIdleTimeout(qint64 timeout); - - /** - * @brief Set Ping interval in milliseconds - * - * When ping interval is greater than zero, a Ping messages will automatically - * be sent. - * - * Note. This should be used by the client only. - * - * @param msecs - */ - void setPingInterval(int msecs); - - void setSmoothEnabled(bool smoothingEnabled); - void setSmoothDrainRate(int smoothDrainRate); - - int artificalLagMs() { return m_artificialLagMs; } - - void setArtificialLagMs(int msecs); - - void setContextId(unsigned int contextId) { m_contextId = contextId; } - -public slots: - /** - * @brief Send a Ping message - * - * Note. Use this function instead of creating the Ping message yourself - * to make sure the roundtrip timer is set correctly! - * This function will not send another ping message until a reply has been received. - */ - void sendPing(); - -signals: - /** - * @brief data reception statistics - * @param number of bytes received since last signal - */ - void bytesReceived(int count); - - /** - * @brief data transmission statistics - * @param count number of bytes sent since last signal - */ - void bytesSent(int count); - - /** - * New message(s) are available. Get them with getPending(). - */ - void messageAvailable(); - - /** - * An unrecognized message was received - * @param len length of the unrecognized message - * @param the unknown message identifier - * @param contextId the context ID of the message - */ - void badData(int len, int type, int contextId); - - void socketError(const QString &errorstring); - - /** - * @brief A reply to our Ping was just received - * @param roundtripTime milliseconds since we sent our ping - */ - void pingPong(qint64 roundtripTime); - - /** - * The server sent a graceful disconnect notification - */ - void gracefulDisconnect(GracefulDisconnect reason, const QString &message); - -private slots: - void readData(); - void dataWritten(qint64); - void sslEncrypted(); - void checkIdleTimeout(); - - void receiveSmoothedMessages(); - - void sendArtificallyLaggedMessages(); - -private: - static constexpr int SMOOTHING_INTERVAL_MSEC = 1000 / 60; - - void enqueueMessages(int count, const drawdance::Message *msgs); - - int haveWholeMessageToRead(); - void writeData(); - bool messagesInOutbox() const; - drawdance::Message dequeueFromOutbox(); - - void handlePing(bool isPong); - void sendPingMsg(bool pong); - - void updateSmoothing(); - - QTcpSocket *m_socket; - - char *m_recvbuffer; // raw message reception buffer - QByteArray m_sendbuffer; // raw message upload buffer - int m_recvbytes; // number of bytes in reception buffer - int m_sentbytes; // number of bytes in upload buffer already sent - - drawdance::MessageList m_inbox; // received (complete) messages - QQueue m_outbox; // messages to be sent - QQueue m_pings; // pings and pongs to be sent - - // Smoothing of received messages. Depending on the server and network - // conditions, messages will arrive in chunks, which causes other people's - // strokes to appear choppy. Smoothing compensates for it, if enabled. - bool m_smoothEnabled; - int m_smoothDrainRate; - QTimer *m_smoothTimer; - drawdance::MessageList m_smoothBuffer; - int m_smoothMessagesToDrain; - unsigned int m_contextId; - - QTimer *m_idleTimer; - QTimer *m_pingTimer; - qint64 m_lastRecvTime; - qint64 m_idleTimeout; - qint64 m_pingSent; - - bool m_gracefullyDisconnecting; - - int m_artificialLagMs; - QVector m_artificialLagTimes; - QVector m_artificialLagMessages; - QTimer *m_artificialLagTimer; -}; - -} - -#endif - diff --git a/src/libclient/net/server.h b/src/libclient/net/server.h index b8863378b0..af6739aa3f 100644 --- a/src/libclient/net/server.h +++ b/src/libclient/net/server.h @@ -1,19 +1,15 @@ // SPDX-License-Identifier: GPL-3.0-or-later - #ifndef DP_NET_SERVER_H #define DP_NET_SERVER_H - -#include #include "libshared/net/protover.h" +#include class QSslCertificate; -namespace drawdance { - class Message; -} - namespace net { +class Message; + /** * \brief Abstract base class for servers interfaces */ @@ -22,8 +18,8 @@ class Server : public QObject { public: enum Security { NO_SECURITY, // No secure connection - NEW_HOST, // Secure connection to a host we haven't seen before - KNOWN_HOST, // Secure connection whose certificate we have seen before + NEW_HOST, // Secure connection to a host we haven't seen before + KNOWN_HOST, // Secure connection whose certificate we have seen before TRUSTED_HOST // A host we have explicitly marked as trusted }; @@ -32,17 +28,17 @@ class Server : public QObject { /** * \brief Send a message to the server */ - virtual void sendMessage(const drawdance::Message &msg) = 0; + virtual void sendMessage(const net::Message &msg) = 0; /** * \brief Send multiple messages to the server */ - virtual void sendMessages(int count, const drawdance::Message *msgs) = 0; + virtual void sendMessages(int count, const net::Message *msgs) = 0; - /** - * @brief Log out from the server - */ - virtual void logout() = 0; + /** + * @brief Log out from the server + */ + virtual void logout() = 0; /** * @brief Is the user in a session @@ -81,11 +77,10 @@ class Server : public QObject { virtual void artificialDisconnect() = 0; signals: - void messagesReceived(int count, drawdance::Message *msgs); + void messagesReceived(int count, net::Message *msgs); }; } #endif - diff --git a/src/libclient/net/servercmd.cpp b/src/libclient/net/servercmd.cpp deleted file mode 100644 index ddb357fe32..0000000000 --- a/src/libclient/net/servercmd.cpp +++ /dev/null @@ -1,128 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -#include "libclient/net/servercmd.h" -#include "libshared/util/qtcompat.h" - -namespace net { - -drawdance::Message ServerCommand::make(const QString &cmd, const QJsonArray &args, const QJsonObject &kwargs) -{ - return ServerCommand { cmd, args, kwargs }.toMessage(); -} - -drawdance::Message ServerCommand::makeKick(int target, bool ban) -{ - Q_ASSERT(target>0 && target<256); - QJsonObject kwargs; - if(ban) - kwargs["ban"] = true; - - return make("kick-user", QJsonArray() << target, kwargs); -} - -drawdance::Message ServerCommand::makeAnnounce(const QString &url, bool privateMode) -{ - QJsonObject kwargs; - if(privateMode) - kwargs["private"] = true; - - return make("announce-session", QJsonArray() << url, kwargs); -} - -drawdance::Message ServerCommand::makeUnannounce(const QString &url) -{ - return make("unlist-session", QJsonArray() << url); -} - -drawdance::Message ServerCommand::makeUnban(int entryId) -{ - return make("remove-ban", QJsonArray() << entryId); -} - -drawdance::Message ServerCommand::makeMute(int target, bool mute) -{ - return make("mute", QJsonArray() << target << mute); -} - -drawdance::Message ServerCommand::toMessage() const -{ - QJsonObject o; - o["cmd"] = cmd; - if(!args.isEmpty()) { - o["args"] = args; - } - if(!kwargs.isEmpty()) { - o["kwargs"] = kwargs; - } - return drawdance::Message::makeServerCommand(0, QJsonDocument{o}); -} - -static ServerReply ServerReplyFromJson(const QJsonDocument &doc) -{ - const QJsonObject o = doc.object(); - const QString typestr = o.value("type").toString(); - - ServerReply r; - - if(typestr == "login") - r.type = ServerReply::ReplyType::Login; - else if(typestr == "msg") - r.type = ServerReply::ReplyType::Message; - else if(typestr == "alert") - r.type = ServerReply::ReplyType::Alert; - else if(typestr == "error") - r.type = ServerReply::ReplyType::Error; - else if(typestr == "result") - r.type = ServerReply::ReplyType::Result; - else if(typestr == "log") - r.type = ServerReply::ReplyType::Log; - else if(typestr == "sessionconf") - r.type = ServerReply::ReplyType::SessionConf; - else if(typestr == "sizelimit") - r.type = ServerReply::ReplyType::SizeLimitWarning; - else if(typestr == "status") - r.type = ServerReply::ReplyType::Status; - else if(typestr == "reset") - r.type = ServerReply::ReplyType::Reset; - else if(typestr == "autoreset") - r.type = ServerReply::ReplyType::ResetRequest; - else if(typestr == "catchup") - r.type = ServerReply::ReplyType::Catchup; - else - r.type = ServerReply::ReplyType::Unknown; - - r.message = o.value("message").toString(); - r.reply = o; - return r; -} - -ServerReply ServerReply::fromMessage(const drawdance::Message &msg) -{ - if(msg.isNull() || msg.type() != DP_MSG_SERVER_COMMAND) { - qWarning("ServerReply::fromMessage: bad message"); - return ServerReply{ - ServerReply::ReplyType::Unknown, - QString(), - QJsonObject() - }; - } - - size_t len; - const char *data = DP_msg_server_command_msg(msg.toServerCommand(), &len); - QByteArray bytes = QByteArray::fromRawData(data, compat::castSize(len)); - - QJsonParseError err; - const QJsonDocument doc = QJsonDocument::fromJson(bytes, &err); - if(err.error != QJsonParseError::NoError) { - qWarning("ServerReply::fromMessage JSON parsing error: %s", qPrintable(err.errorString())); - return ServerReply{ - ServerReply::ReplyType::Unknown, - QString(), - QJsonObject() - }; - } - - return ServerReplyFromJson(doc); -} - -} diff --git a/src/libclient/net/servercmd.h b/src/libclient/net/servercmd.h deleted file mode 100644 index 8a5cf2f02e..0000000000 --- a/src/libclient/net/servercmd.h +++ /dev/null @@ -1,71 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -#ifndef NET_SERVERCMD_H -#define NET_SERVERCMD_H - -#include -#include -#include - -#include "libclient/drawdance/message.h" - -namespace net { - -/** - * @brief A command sent to the server using the (Control) Command message - */ -struct ServerCommand { - QString cmd; - QJsonArray args; - QJsonObject kwargs; - - drawdance::Message toMessage() const; - - // Convenience functions_ - static drawdance::Message make(const QString &cmd, const QJsonArray &args=QJsonArray(), const QJsonObject &kwargs=QJsonObject()); - - //! Kick (and optionally ban) a user from the session - static drawdance::Message makeKick(int target, bool ban); - - //! Remove a ban entry - static drawdance::Message makeUnban(int entryId); - - //! (Un)mute a user - static drawdance::Message makeMute(int target, bool mute); - - //! Request the server to announce this session at a listing server - static drawdance::Message makeAnnounce(const QString &url, bool privateMode); - - //! Request the server to remove an announcement at the listing server - static drawdance::Message makeUnannounce(const QString &url); -}; - -/** - * @brief A reply or notification from the server received with the Command message - */ -struct ServerReply { - enum class ReplyType { - Unknown, - Login, // used during the login phase - Message, // general chat type notifcation message - Alert, // urgen notification message - Error, // error occurred - Result, // comand result - Log, // server log message - SessionConf, // session configuration update - SizeLimitWarning, // session history size nearing limit (deprecated) - Status, // Periodic status update - Reset, // session reset state - Catchup, // number of messages queued for upload (use for progress bars) - ResetRequest, // request client to perform a reset - } type; - QString message; - QJsonObject reply; - - static ServerReply fromMessage(const drawdance::Message &msg); - static ServerReply fromJson(const QJsonDocument &doc); - QJsonDocument toJson() const; -}; - -} - -#endif diff --git a/src/libclient/net/tcpserver.cpp b/src/libclient/net/tcpserver.cpp index a592ad2f28..832bca8dc4 100644 --- a/src/libclient/net/tcpserver.cpp +++ b/src/libclient/net/tcpserver.cpp @@ -1,21 +1,22 @@ // SPDX-License-Identifier: GPL-3.0-or-later - #include "libclient/net/tcpserver.h" +#include "cmake-config/config.h" #include "libclient/net/login.h" -#include "libclient/net/messagequeue.h" -#include "libclient/net/servercmd.h" +#include "libshared/net/messagequeue.h" +#include "libshared/net/servercmd.h" #include "libshared/util/qtcompat.h" -#include "cmake-config/config.h" - #include -#include #include +#include namespace net { -TcpServer::TcpServer(int timeoutSecs, QObject *parent) : - Server(parent), m_loginstate(nullptr), m_securityLevel(NO_SECURITY), - m_localDisconnect(false), m_supportsPersistence(false) +TcpServer::TcpServer(int timeoutSecs, QObject *parent) + : Server(parent) + , m_loginstate(nullptr) + , m_securityLevel(NO_SECURITY) + , m_localDisconnect(false) + , m_supportsPersistence(false) { m_socket = new QSslSocket(this); @@ -23,24 +24,35 @@ TcpServer::TcpServer(int timeoutSecs, QObject *parent) : sslconf.setSslOption(QSsl::SslOptionDisableCompression, false); m_socket->setSslConfiguration(sslconf); - m_msgqueue = new MessageQueue(m_socket, this); + m_msgqueue = new MessageQueue(m_socket, true, this); m_msgqueue->setIdleTimeout(timeoutSecs * 1000); m_msgqueue->setPingInterval(15 * 1000); - connect(m_socket, &QSslSocket::disconnected, this, &TcpServer::handleDisconnect); + connect( + m_socket, &QSslSocket::disconnected, this, + &TcpServer::handleDisconnect); connect(m_socket, compat::SocketError, this, &TcpServer::handleSocketError); - connect(m_socket, &QSslSocket::stateChanged, this, [this](QAbstractSocket::SocketState state) { - if(state==QAbstractSocket::ClosingState) - emit loggingOut(); - }); - - connect(m_msgqueue, &MessageQueue::messageAvailable, this, &TcpServer::handleMessage); - connect(m_msgqueue, &MessageQueue::bytesReceived, this, &TcpServer::bytesReceived); + connect( + m_socket, &QSslSocket::stateChanged, this, + [this](QAbstractSocket::SocketState state) { + if(state == QAbstractSocket::ClosingState) + emit loggingOut(); + }); + + connect( + m_msgqueue, &MessageQueue::messageAvailable, this, + &TcpServer::handleMessage); + connect( + m_msgqueue, &MessageQueue::bytesReceived, this, + &TcpServer::bytesReceived); connect(m_msgqueue, &MessageQueue::bytesSent, this, &TcpServer::bytesSent); - connect(m_msgqueue, &MessageQueue::badData, this, &TcpServer::handleBadData); + connect( + m_msgqueue, &MessageQueue::badData, this, &TcpServer::handleBadData); connect(m_msgqueue, &MessageQueue::pingPong, this, &TcpServer::lagMeasured); - connect(m_msgqueue, &MessageQueue::gracefulDisconnect, this, &TcpServer::gracefullyDisconnecting); + connect( + m_msgqueue, &MessageQueue::gracefulDisconnect, this, + &TcpServer::gracefullyDisconnecting); } void TcpServer::login(LoginHandler *login) @@ -48,13 +60,15 @@ void TcpServer::login(LoginHandler *login) m_loginstate = login; m_loginstate->setParent(this); m_loginstate->setServer(this); - m_socket->connectToHost(login->url().host(), login->url().port(cmake_config::proto::port())); + m_socket->connectToHost( + login->url().host(), login->url().port(cmake_config::proto::port())); } void TcpServer::logout() { m_localDisconnect = true; - m_msgqueue->sendDisconnect(MessageQueue::GracefulDisconnect::Shutdown, QString()); + m_msgqueue->sendDisconnect( + MessageQueue::GracefulDisconnect::Shutdown, QString()); } void TcpServer::artificialDisconnect() @@ -67,12 +81,12 @@ int TcpServer::uploadQueueBytes() const return m_msgqueue->uploadQueueBytes(); } -void TcpServer::sendMessage(const drawdance::Message &msg) +void TcpServer::sendMessage(const net::Message &msg) { m_msgqueue->send(msg); } -void TcpServer::sendMessages(int count, const drawdance::Message *msgs) +void TcpServer::sendMessages(int count, const net::Message *msgs) { m_msgqueue->sendMultiple(count, msgs); } @@ -87,7 +101,8 @@ void TcpServer::handleMessage() // since the inbox may contain messages not belonging to // the login handshake anymore. for(int i = 0; i < count; ++i) { - const ServerReply sr = ServerReply::fromMessage(m_receiveBuffer[i]); + const ServerReply sr = + ServerReply::fromMessage(m_receiveBuffer[i]); const bool expectMoreLogin = m_loginstate->receiveMessage(sr); const int offset = i + 1; if(!expectMoreLogin && offset < count) { @@ -105,9 +120,12 @@ void TcpServer::handleMessage() void TcpServer::handleBadData(int len, int type, int contextId) { - qWarning() << "Received" << len << "bytes of unknown or invalid message type" << type << "from context ID" << contextId; + qWarning() << "Received" << len + << "bytes of unknown or invalid message type" << type + << "from context ID" << contextId; if(type < 64 || contextId == 0) { - // If message type is Transparent, the bad data came from the server. Something is wrong for sure. + // If message type is Transparent, the bad data came from the server. + // Something is wrong for sure. m_error = tr("Received invalid data"); m_socket->abort(); } else { @@ -147,21 +165,19 @@ void TcpServer::loginFailure(const QString &message, const QString &errorcode) void TcpServer::loginSuccess() { - qDebug() << "logged in to session" << m_loginstate->url() << ". Got user id" << m_loginstate->userId(); + qDebug() << "logged in to session" << m_loginstate->url() << ". Got user id" + << m_loginstate->userId(); m_supportsPersistence = m_loginstate->supportsPersistence(); m_supportsAbuseReports = m_loginstate->supportsAbuseReports(); m_msgqueue->setContextId(m_loginstate->userId()); emit loggedIn( - m_loginstate->url(), - m_loginstate->userId(), + m_loginstate->url(), m_loginstate->userId(), m_loginstate->mode() == LoginHandler::Mode::Join, - m_loginstate->isAuthenticated(), - m_loginstate->hasUserFlag("MOD"), + m_loginstate->isAuthenticated(), m_loginstate->hasUserFlag("MOD"), !m_loginstate->sessionFlags().contains("NOAUTORESET"), - m_loginstate->compatibilityMode(), - m_loginstate->joinPassword()); + m_loginstate->compatibilityMode(), m_loginstate->joinPassword()); m_loginstate->deleteLater(); m_loginstate = nullptr; diff --git a/src/libclient/net/tcpserver.h b/src/libclient/net/tcpserver.h index 068df2dcd7..0f74be13b5 100644 --- a/src/libclient/net/tcpserver.h +++ b/src/libclient/net/tcpserver.h @@ -1,11 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-or-later - #ifndef DP_NET_TCPSERVER_H #define DP_NET_TCPSERVER_H - #include "libclient/net/server.h" -#include "libclient/net/messagequeue.h" - +#include "libshared/net/messagequeue.h" #include class QSslSocket; @@ -14,18 +11,18 @@ namespace net { class LoginHandler; -class TcpServer final : public Server -{ +class TcpServer final : public Server { Q_OBJECT friend class LoginHandler; + public: - explicit TcpServer(int timeoutSecs, QObject *parent=nullptr); + explicit TcpServer(int timeoutSecs, QObject *parent = nullptr); void login(LoginHandler *login); void logout() override; - void sendMessage(const drawdance::Message &msg) override; - void sendMessages(int count, const drawdance::Message *msgs) override; + void sendMessage(const net::Message &msg) override; + void sendMessages(int count, const net::Message *msgs) override; bool isLoggedIn() const override { return m_loginstate == nullptr; } @@ -37,13 +34,28 @@ class TcpServer final : public Server QSslCertificate hostCertificate() const override; bool supportsPersistence() const override { return m_supportsPersistence; } - bool supportsAbuseReports() const override { return m_supportsAbuseReports; } - - void setSmoothEnabled(bool smoothEnabled) override { m_msgqueue->setSmoothEnabled(smoothEnabled); } - void setSmoothDrainRate(int smoothDrainRate) override { m_msgqueue->setSmoothDrainRate(smoothDrainRate); } - - int artificialLagMs() const override { return m_msgqueue->artificalLagMs(); } - void setArtificialLagMs(int msecs) override { m_msgqueue->setArtificialLagMs(msecs); } + bool supportsAbuseReports() const override + { + return m_supportsAbuseReports; + } + + void setSmoothEnabled(bool smoothEnabled) override + { + m_msgqueue->setSmoothEnabled(smoothEnabled); + } + void setSmoothDrainRate(int smoothDrainRate) override + { + m_msgqueue->setSmoothDrainRate(smoothDrainRate); + } + + int artificialLagMs() const override + { + return m_msgqueue->artificalLagMs(); + } + void setArtificialLagMs(int msecs) override + { + m_msgqueue->setArtificialLagMs(msecs); + } void artificialDisconnect() override; @@ -52,8 +64,10 @@ class TcpServer final : public Server const QUrl &url, uint8_t userid, bool join, bool auth, bool moderator, bool hasAutoreset, bool compatibilityMode, const QString &joinPassword); void loggingOut(); - void gracefullyDisconnecting(MessageQueue::GracefulDisconnect, const QString &message); - void serverDisconnected(const QString &message, const QString &errorcode, bool localDisconnect); + void gracefullyDisconnecting( + MessageQueue::GracefulDisconnect, const QString &message); + void serverDisconnected( + const QString &message, const QString &errorcode, bool localDisconnect); void bytesReceived(int); void bytesSent(int); @@ -73,7 +87,7 @@ private slots: private: QSslSocket *m_socket; MessageQueue *m_msgqueue; - drawdance::MessageList m_receiveBuffer; + net::MessageList m_receiveBuffer; LoginHandler *m_loginstate; QString m_error, m_errorcode; Security m_securityLevel; diff --git a/src/libclient/settings.h b/src/libclient/settings.h index 7b891a4b09..a9bff161ce 100644 --- a/src/libclient/settings.h +++ b/src/libclient/settings.h @@ -6,6 +6,7 @@ #include "cmake-config/config.h" #include "libclient/canvas/paintengine.h" #include "libclient/parentalcontrols/parentalcontrols.h" +#include "libshared/net/messagequeue.h" #include #include @@ -44,9 +45,6 @@ inline constexpr int defaultSmoothing = 3; inline constexpr int maxSmoothing = 20; -inline constexpr int defaultMessageQueueDrainRate = 20; -inline constexpr int maxMessageQueueDrainRate = 60; - inline constexpr qreal zoomMin = 0.05; inline constexpr qreal zoomMax = 64.0; inline constexpr qreal zoomSoftMin = 0.125; diff --git a/src/libclient/settings_table.h b/src/libclient/settings_table.h index e2a882034d..754fd0c524 100644 --- a/src/libclient/settings_table.h +++ b/src/libclient/settings_table.h @@ -1,7 +1,7 @@ #include "libclient/settings_table_macros.h" SETTING(autoSaveInterval , AutoSaveInterval , "settings/autosave" , 5000) -SETTING(messageQueueDrainRate , MessageQueueDrainRate , "settings/messagequeuedrainrate" , defaultMessageQueueDrainRate) +SETTING(messageQueueDrainRate , MessageQueueDrainRate , "settings/messagequeuedrainrate" , net::MessageQueue::DEFAULT_SMOOTH_DRAIN_RATE) SETTING(parentalControlsAutoTag , ParentalControlsAutoTag , "pc/autotag" , true) SETTING(parentalControlsForceCensor , ParentalControlsForceCensor , "pc/noUncensoring" , false) SETTING(parentalControlsLevel , ParentalControlsLevel , "pc/level" , parentalcontrols::Level::Unrestricted) diff --git a/src/libclient/tools/annotation.cpp b/src/libclient/tools/annotation.cpp index ceeb0d52ed..bbc3c5ed89 100644 --- a/src/libclient/tools/annotation.cpp +++ b/src/libclient/tools/annotation.cpp @@ -1,19 +1,18 @@ // SPDX-License-Identifier: GPL-3.0-or-later - +#include "libclient/tools/annotation.h" #include "libclient/canvas/canvasmodel.h" #include "libclient/canvas/paintengine.h" #include "libclient/net/client.h" -#include "libclient/drawdance/message.h" - +#include "libclient/net/message.h" #include "libclient/tools/toolcontroller.h" -#include "libclient/tools/annotation.h" - #include namespace tools { Annotation::Annotation(ToolController &owner) - : Tool(owner, ANNOTATION, QCursor(QPixmap(":cursors/text.png"), 2, 2), false, false, true) + : Tool( + owner, ANNOTATION, QCursor(QPixmap(":cursors/text.png"), 2, 2), false, + false, true) , m_selectedId{0} { } @@ -22,7 +21,7 @@ Annotation::Annotation(ToolController &owner) * The annotation tool has fairly complex needs. Clicking on an existing * annotation selects it, otherwise a new annotation is started. */ -void Annotation::begin(const canvas::Point& point, bool right, float zoom) +void Annotation::begin(const canvas::Point &point, bool right, float zoom) { if(right) { m_selectedId = 0; @@ -34,18 +33,23 @@ void Annotation::begin(const canvas::Point& point, bool right, float zoom) m_p2 = point; const int handleSize = qRound(qMax(10.0, 10.0 / zoom) / 2.0); - drawdance::Annotation selection = m_owner.model()->paintEngine()->getAnnotationAt(point.x(), point.y(), handleSize); + drawdance::Annotation selection = + m_owner.model()->paintEngine()->getAnnotationAt( + point.x(), point.y(), handleSize); if(selection.isNull()) { // No annotation, start creating a new one - if(!m_owner.model()->aclState()->canUseFeature(DP_FEATURE_CREATE_ANNOTATION)) { + if(!m_owner.model()->aclState()->canUseFeature( + DP_FEATURE_CREATE_ANNOTATION)) { m_handle = Handle::Outside; return; } - m_selectedId = m_owner.model()->paintEngine()->findAvailableAnnotationId(m_owner.model()->localUserId()); + m_selectedId = + m_owner.model()->paintEngine()->findAvailableAnnotationId( + m_owner.model()->localUserId()); m_handle = Handle::BottomRight; - m_shape = QRect { m_p1.toPoint(), QSize{1, 1} }; + m_shape = QRect{m_p1.toPoint(), QSize{1, 1}}; m_isNew = true; // Note: The tool functions perfectly even if nothing happens in @@ -57,7 +61,8 @@ void Annotation::begin(const canvas::Point& point, bool right, float zoom) m_selectedId = selection.id(); m_shape = selection.bounds(); - if(selection.protect() && !m_owner.model()->aclState()->amOperator() && (m_selectedId >> 8) != m_owner.client()->myId()) { + if(selection.protect() && !m_owner.model()->aclState()->amOperator() && + (m_selectedId >> 8) != m_owner.client()->myId()) { m_handle = Handle::Outside; } else { m_handle = handleAt(m_shape, point.toPoint(), handleSize); @@ -67,21 +72,19 @@ void Annotation::begin(const canvas::Point& point, bool right, float zoom) } } -Annotation::Handle Annotation::handleAt(const QRect &rect, const QPoint &point, int handleSize) +Annotation::Handle +Annotation::handleAt(const QRect &rect, const QPoint &point, int handleSize) { - const QRect R { - rect.x()-handleSize/2, - rect.y()-handleSize/2, - rect.width()+handleSize, - rect.height()+handleSize - }; + const QRect R{ + rect.x() - handleSize / 2, rect.y() - handleSize / 2, + rect.width() + handleSize, rect.height() + handleSize}; const QPointF p = point - R.topLeft(); if(p.x() < handleSize) { if(p.y() < handleSize) return Handle::TopLeft; - else if(p.y() > R.height()-handleSize) + else if(p.y() > R.height() - handleSize) return Handle::BottomLeft; return Handle::Left; } else if(p.x() > R.width() - handleSize) { @@ -101,7 +104,7 @@ Annotation::Handle Annotation::handleAt(const QRect &rect, const QPoint &point, /** * Change the shape of the selected annotation. */ -void Annotation::motion(const canvas::Point& point, bool constrain, bool center) +void Annotation::motion(const canvas::Point &point, bool constrain, bool center) { Q_UNUSED(constrain); Q_UNUSED(center); @@ -109,45 +112,90 @@ void Annotation::motion(const canvas::Point& point, bool constrain, bool center) return; const QPoint delta = (point - m_p2).toPoint(); - if(delta.manhattanLength()==0) + if(delta.manhattanLength() == 0) return; m_p2 = point; switch(m_handle) { - case Handle::Outside: return; - case Handle::Inside: m_shape.translate(delta); break; - case Handle::TopLeft: m_shape.adjust(delta.x(), delta.y(), 0, 0); break; - case Handle::TopRight: m_shape.adjust(0, delta.y(), delta.x(), 0); break; - case Handle::BottomRight: m_shape.adjust(0, 0, delta.x(), delta.y()); break; - case Handle::BottomLeft: m_shape.adjust(delta.x(), 0, 0, delta.y()); break; - case Handle::Top: m_shape.adjust(0, delta.y(), 0, 0); break; - case Handle::Right: m_shape.adjust(0, 0, delta.x(), 0); break; - case Handle::Bottom: m_shape.adjust(0, 0, 0, delta.y()); break; - case Handle::Left: m_shape.adjust(delta.x(), 0, 0, 0); break; + case Handle::Outside: + return; + case Handle::Inside: + m_shape.translate(delta); + break; + case Handle::TopLeft: + m_shape.adjust(delta.x(), delta.y(), 0, 0); + break; + case Handle::TopRight: + m_shape.adjust(0, delta.y(), delta.x(), 0); + break; + case Handle::BottomRight: + m_shape.adjust(0, 0, delta.x(), delta.y()); + break; + case Handle::BottomLeft: + m_shape.adjust(delta.x(), 0, 0, delta.y()); + break; + case Handle::Top: + m_shape.adjust(0, delta.y(), 0, 0); + break; + case Handle::Right: + m_shape.adjust(0, 0, delta.x(), 0); + break; + case Handle::Bottom: + m_shape.adjust(0, 0, 0, delta.y()); + break; + case Handle::Left: + m_shape.adjust(delta.x(), 0, 0, 0); + break; } if(m_shape.left() > m_shape.right() || m_shape.top() > m_shape.bottom()) { if(m_shape.left() > m_shape.right()) { switch(m_handle) { - case Handle::TopLeft: m_handle = Handle::TopRight; break; - case Handle::TopRight: m_handle = Handle::TopLeft; break; - case Handle::BottomRight: m_handle = Handle::BottomLeft; break; - case Handle::BottomLeft: m_handle = Handle::BottomRight; break; - case Handle::Left: m_handle = Handle::Right; break; - case Handle::Right: m_handle = Handle::Left; break; - default: break; + case Handle::TopLeft: + m_handle = Handle::TopRight; + break; + case Handle::TopRight: + m_handle = Handle::TopLeft; + break; + case Handle::BottomRight: + m_handle = Handle::BottomLeft; + break; + case Handle::BottomLeft: + m_handle = Handle::BottomRight; + break; + case Handle::Left: + m_handle = Handle::Right; + break; + case Handle::Right: + m_handle = Handle::Left; + break; + default: + break; } } if(m_shape.top() > m_shape.bottom()) { switch(m_handle) { - case Handle::TopLeft: m_handle = Handle::BottomLeft; break; - case Handle::TopRight: m_handle = Handle::BottomRight; break; - case Handle::BottomRight: m_handle = Handle::TopRight; break; - case Handle::BottomLeft: m_handle = Handle::TopRight; break; - case Handle::Top: m_handle = Handle::Bottom; break; - case Handle::Bottom: m_handle = Handle::Top; break; - default: break; + case Handle::TopLeft: + m_handle = Handle::BottomLeft; + break; + case Handle::TopRight: + m_handle = Handle::BottomRight; + break; + case Handle::BottomRight: + m_handle = Handle::TopRight; + break; + case Handle::BottomLeft: + m_handle = Handle::TopRight; + break; + case Handle::Top: + m_handle = Handle::Bottom; + break; + case Handle::Bottom: + m_handle = Handle::Top; + break; + default: + break; } } @@ -167,26 +215,29 @@ void Annotation::end() return; const uint8_t contextId = m_owner.client()->myId(); - drawdance::Message msg; + net::Message msg; if(!m_isNew) { if(m_p1.toPoint() != m_p2.toPoint()) { - msg = drawdance::Message::makeAnnotationReshape( - contextId, m_selectedId, m_shape.x(), m_shape.y(), m_shape.width(), m_shape.height()); + msg = net::makeAnnotationReshapeMessage( + contextId, m_selectedId, m_shape.x(), m_shape.y(), + m_shape.width(), m_shape.height()); } } else if(m_handle != Handle::Outside) { if(m_shape.width() < 10 && m_shape.height() < 10) { - // User created a tiny annotation, probably by clicking rather than dragging. - // Create a nice and big annotation box rather than a minimum size one. + // User created a tiny annotation, probably by clicking rather than + // dragging. Create a nice and big annotation box rather than a + // minimum size one. m_shape.setSize(QSize(160, 60)); } - msg = drawdance::Message::makeAnnotationCreate( - contextId, m_selectedId, m_shape.x(), m_shape.y(), m_shape.width(), m_shape.height()); + msg = net::makeAnnotationCreateMessage( + contextId, m_selectedId, m_shape.x(), m_shape.y(), m_shape.width(), + m_shape.height()); } if(!msg.isNull()) { - drawdance::Message messages[] = { - drawdance::Message::makeUndoPoint(contextId), + net::Message messages[] = { + net::makeUndoPointMessage(contextId), msg, }; m_owner.client()->sendMessages(DP_ARRAY_LENGTH(messages), messages); diff --git a/src/libclient/tools/annotation.h b/src/libclient/tools/annotation.h index 494b076738..c4531d0a40 100644 --- a/src/libclient/tools/annotation.h +++ b/src/libclient/tools/annotation.h @@ -1,10 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later - #ifndef TOOLS_ANNOTATION_H #define TOOLS_ANNOTATION_H - #include "libclient/tools/tool.h" - #include namespace tools { @@ -16,8 +13,11 @@ class Annotation final : public Tool { public: Annotation(ToolController &owner); - void begin(const canvas::Point& point, bool right, float zoom) override; - void motion(const canvas::Point& point, bool constrain, bool center) override; + void begin(const canvas::Point &point, bool right, float zoom) override; + + void + motion(const canvas::Point &point, bool constrain, bool center) override; + void end() override; private: @@ -56,4 +56,3 @@ class Annotation final : public Tool { } #endif - diff --git a/src/libclient/tools/floodfill.cpp b/src/libclient/tools/floodfill.cpp index e10480ff8a..b3cbb07d22 100644 --- a/src/libclient/tools/floodfill.cpp +++ b/src/libclient/tools/floodfill.cpp @@ -1,12 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-or-later - #include "libclient/tools/floodfill.h" -#include "libclient/tools/toolcontroller.h" - #include "libclient/canvas/canvasmodel.h" #include "libclient/canvas/paintengine.h" #include "libclient/net/client.h" - +#include "libclient/tools/toolcontroller.h" #include #include @@ -126,9 +123,9 @@ void FloodFill::floodFillFinished(Task *task) DP_FloodFillResult result = task->result(); if(result == DP_FLOOD_FILL_SUCCESS) { uint8_t contextId = m_owner.model()->localUserId(); - drawdance::MessageList msgs; - msgs.append(drawdance::Message::makeUndoPoint(contextId)); - drawdance::Message::makePutImages( + net::MessageList msgs; + msgs.append(net::makeUndoPointMessage(contextId)); + net::makePutImageMessages( msgs, contextId, task->targetLayerId(), m_blendMode, task->x(), task->y(), task->img()); m_owner.client()->sendMessages(msgs.count(), msgs.constData()); diff --git a/src/libclient/tools/floodfill.h b/src/libclient/tools/floodfill.h index a89994be90..99f5c29331 100644 --- a/src/libclient/tools/floodfill.h +++ b/src/libclient/tools/floodfill.h @@ -1,28 +1,28 @@ // SPDX-License-Identifier: GPL-3.0-or-later - #ifndef TOOLS_FLOODFILL_H #define TOOLS_FLOODFILL_H - #include "libclient/tools/tool.h" - -#include #include +#include namespace tools { -class FloodFill final : public Tool -{ +class FloodFill final : public Tool { public: FloodFill(ToolController &owner); - void begin(const canvas::Point& point, bool right, float zoom) override; - void motion(const canvas::Point& point, bool constrain, bool center) override; + void begin(const canvas::Point &point, bool right, float zoom) override; + void + motion(const canvas::Point &point, bool constrain, bool center) override; void end() override; void cancelMultipart() override; void setTolerance(qreal tolerance) { m_tolerance = tolerance; } void setExpansion(int expansion) { m_expansion = expansion; } - void setFeatherRadius(int featherRadius) { m_featherRadius = featherRadius; } + void setFeatherRadius(int featherRadius) + { + m_featherRadius = featherRadius; + } void setSize(int size) { m_size = size; } void setGap(int gap) { m_gap = gap; } void setLayerId(int layerId) { m_layerId = layerId; } diff --git a/src/libclient/tools/freehand.cpp b/src/libclient/tools/freehand.cpp index 38b0cb79e3..33ede15bab 100644 --- a/src/libclient/tools/freehand.cpp +++ b/src/libclient/tools/freehand.cpp @@ -1,18 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-or-later - -extern "C" { -#include -#include -} - +#include "libclient/tools/freehand.h" #include "libclient/canvas/canvasmodel.h" #include "libclient/canvas/paintengine.h" #include "libclient/net/client.h" - #include "libclient/tools/toolcontroller.h" -#include "libclient/tools/freehand.h" - -#include "libshared/net/undo.h" #include using std::placeholders::_1; @@ -20,7 +11,9 @@ using std::placeholders::_1; namespace tools { Freehand::Freehand(ToolController &owner, bool isEraser) - : Tool(owner, isEraser ? ERASER : FREEHAND, Qt::CrossCursor, true, true, false) + : Tool( + owner, isEraser ? ERASER : FREEHAND, Qt::CrossCursor, true, true, + false) , m_pollTimer{} , m_brushEngine{std::bind(&Freehand::pollControl, this, _1)} , m_drawing(false) @@ -28,16 +21,14 @@ Freehand::Freehand(ToolController &owner, bool isEraser) m_pollTimer.setSingleShot(false); m_pollTimer.setTimerType(Qt::PreciseTimer); m_pollTimer.setInterval(15); - QObject::connect(&m_pollTimer, &QTimer::timeout, [this](){ + QObject::connect(&m_pollTimer, &QTimer::timeout, [this]() { poll(); }); } -Freehand::~Freehand() -{ -} +Freehand::~Freehand() {} -void Freehand::begin(const canvas::Point& point, bool right, float zoom) +void Freehand::begin(const canvas::Point &point, bool right, float zoom) { Q_ASSERT(!m_drawing); if(right) { @@ -56,14 +47,15 @@ void Freehand::begin(const canvas::Point& point, bool right, float zoom) m_zoom = zoom; } -void Freehand::motion(const canvas::Point& point, bool constrain, bool center) +void Freehand::motion(const canvas::Point &point, bool constrain, bool center) { Q_UNUSED(constrain); Q_UNUSED(center); if(!m_drawing) return; - drawdance::CanvasState canvasState = m_owner.model()->paintEngine()->sampleCanvasState(); + drawdance::CanvasState canvasState = + m_owner.model()->paintEngine()->sampleCanvasState(); if(m_firstPoint) { m_firstPoint = false; @@ -89,7 +81,8 @@ void Freehand::end() m_brushEngine.strokeTo(m_start, canvasState); } - m_brushEngine.endStroke(QDateTime::currentMSecsSinceEpoch(), canvasState, true); + m_brushEngine.endStroke( + QDateTime::currentMSecsSinceEpoch(), canvasState, true); m_brushEngine.sendMessagesTo(m_owner.client()); } } @@ -112,10 +105,10 @@ void Freehand::pollControl(bool enable) void Freehand::poll() { - drawdance::CanvasState canvasState = m_owner.model()->paintEngine()->sampleCanvasState(); + drawdance::CanvasState canvasState = + m_owner.model()->paintEngine()->sampleCanvasState(); m_brushEngine.poll(QDateTime::currentMSecsSinceEpoch(), canvasState); m_brushEngine.sendMessagesTo(m_owner.client()); } } - diff --git a/src/libclient/tools/freehand.h b/src/libclient/tools/freehand.h index 7cab41594a..d45805be3f 100644 --- a/src/libclient/tools/freehand.h +++ b/src/libclient/tools/freehand.h @@ -1,24 +1,23 @@ // SPDX-License-Identifier: GPL-3.0-or-later - #ifndef TOOLS_FREEHAND_H #define TOOLS_FREEHAND_H - -#include "libclient/tools/tool.h" #include "libclient/drawdance/brushengine.h" -#include "libclient/drawdance/message.h" +#include "libclient/tools/tool.h" #include namespace tools { //! Freehand brush tool -class Freehand final : public Tool -{ +class Freehand final : public Tool { public: Freehand(ToolController &owner, bool isEraser); ~Freehand() override; - void begin(const canvas::Point& point, bool right, float zoom) override; - void motion(const canvas::Point& point, bool constrain, bool center) override; + void begin(const canvas::Point &point, bool right, float zoom) override; + + void + motion(const canvas::Point &point, bool constrain, bool center) override; + void end() override; bool allowSmoothing() const override { return true; } @@ -40,4 +39,3 @@ class Freehand final : public Tool } #endif - diff --git a/src/libclient/tools/laser.cpp b/src/libclient/tools/laser.cpp index 1893c3167a..736a21e047 100644 --- a/src/libclient/tools/laser.cpp +++ b/src/libclient/tools/laser.cpp @@ -1,19 +1,20 @@ // SPDX-License-Identifier: GPL-3.0-or-later - -#include "libclient/drawdance/message.h" +#include "libclient/tools/laser.h" #include "libclient/net/client.h" - +#include "libclient/net/message.h" #include "libclient/tools/toolcontroller.h" -#include "libclient/tools/laser.h" - #include namespace tools { LaserPointer::LaserPointer(ToolController &owner) - : Tool(owner, LASERPOINTER, QCursor(QPixmap(":cursors/arrow.png"), 0, 0), false, true, false), - m_persistence(1), m_drawing(false) -{} + : Tool( + owner, LASERPOINTER, QCursor(QPixmap(":cursors/arrow.png"), 0, 0), + false, true, false) + , m_persistence(1) + , m_drawing(false) +{ +} void LaserPointer::begin(const canvas::Point &point, bool right, float zoom) { @@ -28,19 +29,20 @@ void LaserPointer::begin(const canvas::Point &point, bool right, float zoom) net::Client *client = m_owner.client(); uint8_t contextId = client->myId(); uint32_t color = m_owner.activeBrush().qColor().rgb(); - drawdance::Message messages[] = { - drawdance::Message::makeLaserTrail(contextId, color, m_persistence), - drawdance::Message::makeMovePointer(contextId, point.x() * 4, point.y() * 4), + net::Message messages[] = { + net::makeLaserTrailMessage(contextId, color, m_persistence), + net::makeMovePointerMessage(contextId, point.x() * 4, point.y() * 4), }; client->sendMessages(DP_ARRAY_LENGTH(messages), messages); } -void LaserPointer::motion(const canvas::Point &point, bool constrain, bool center) +void LaserPointer::motion( + const canvas::Point &point, bool constrain, bool center) { Q_UNUSED(constrain); Q_UNUSED(center); if(m_drawing) { - m_owner.client()->sendMessage(drawdance::Message::makeMovePointer( + m_owner.client()->sendMessage(net::makeMovePointerMessage( m_owner.client()->myId(), point.x() * 4, point.y() * 4)); } } @@ -49,10 +51,9 @@ void LaserPointer::end() { if(m_drawing) { m_drawing = false; - m_owner.client()->sendMessage(drawdance::Message::makeLaserTrail( - m_owner.client()->myId(), 0, 0)); + m_owner.client()->sendMessage( + net::makeLaserTrailMessage(m_owner.client()->myId(), 0, 0)); } } } - diff --git a/src/libclient/tools/laser.h b/src/libclient/tools/laser.h index 9764188441..45411f9ca9 100644 --- a/src/libclient/tools/laser.h +++ b/src/libclient/tools/laser.h @@ -1,8 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later - #ifndef TOOLS_LASER_H #define TOOLS_LASER_H - #include "libclient/tools/tool.h" namespace tools { @@ -11,8 +9,11 @@ class LaserPointer final : public Tool { public: LaserPointer(ToolController &owner); - void begin(const canvas::Point& point, bool right, float zoom) override; - void motion(const canvas::Point& point, bool constrain, bool center) override; + void begin(const canvas::Point &point, bool right, float zoom) override; + + void + motion(const canvas::Point &point, bool constrain, bool center) override; + void end() override; bool allowSmoothing() const override { return true; } @@ -27,4 +28,3 @@ class LaserPointer final : public Tool { } #endif - diff --git a/src/libclient/tools/selection.cpp b/src/libclient/tools/selection.cpp index 585bd05bf8..5f4c1736e2 100644 --- a/src/libclient/tools/selection.cpp +++ b/src/libclient/tools/selection.cpp @@ -1,18 +1,15 @@ // SPDX-License-Identifier: GPL-3.0-or-later - +#include "libclient/tools/selection.h" #include "libclient/canvas/canvasmodel.h" #include "libclient/canvas/paintengine.h" #include "libclient/net/client.h" - -#include "libclient/tools/selection.h" #include "libclient/tools/toolcontroller.h" #include "libclient/tools/utils.h" - +#include #include -#include #include #include -#include +#include namespace tools { @@ -22,7 +19,8 @@ void SelectionTool::begin(const canvas::Point &point, bool right, float zoom) return; } - canvas::Selection *sel = m_allowTransform ? m_owner.model()->selection() : nullptr; + canvas::Selection *sel = + m_allowTransform ? m_owner.model()->selection() : nullptr; if(sel) m_handle = sel->handleAt(point, zoom); else @@ -34,9 +32,10 @@ void SelectionTool::begin(const canvas::Point &point, bool right, float zoom) if(m_handle == canvas::Selection::Handle::Outside) { net::Client *client = m_owner.client(); - if(sel && sel->pasteOrMoveToCanvas(m_messages, client->myId(), - m_owner.activeLayer(), m_owner.selectInterpolation(), - client->isCompatibilityMode())) { + if(sel && + sel->pasteOrMoveToCanvas( + m_messages, client->myId(), m_owner.activeLayer(), + m_owner.selectInterpolation(), client->isCompatibilityMode())) { client->sendMessages(m_messages.count(), m_messages.constData()); m_messages.clear(); } @@ -49,7 +48,8 @@ void SelectionTool::begin(const canvas::Point &point, bool right, float zoom) } } -void SelectionTool::motion(const canvas::Point &point, bool constrain, bool center) +void SelectionTool::motion( + const canvas::Point &point, bool constrain, bool center) { canvas::Selection *sel = m_owner.model()->selection(); if(!sel) @@ -57,11 +57,12 @@ void SelectionTool::motion(const canvas::Point &point, bool constrain, bool cent m_end = point; - if(m_handle==canvas::Selection::Handle::Outside) { + if(m_handle == canvas::Selection::Handle::Outside) { newSelectionMotion(point, constrain, center); } else { - if(sel->pasteImage().isNull() && !m_owner.model()->aclState()->isLayerLocked(m_owner.activeLayer())) { + if(sel->pasteImage().isNull() && + !m_owner.model()->aclState()->isLayerLocked(m_owner.activeLayer())) { startMove(); } @@ -76,7 +77,8 @@ void SelectionTool::end() return; // The shape must be closed after the end of the selection operation - if(!m_owner.model()->selection()->closeShape(QRectF(QPointF(), m_owner.model()->size()))) { + if(!m_owner.model()->selection()->closeShape( + QRectF(QPointF(), m_owner.model()->size()))) { // Clear selection if it was entirely outside the canvas m_owner.model()->setSelection(nullptr); return; @@ -111,10 +113,12 @@ void SelectionTool::finishMultipart() { canvas::Selection *sel = m_owner.model()->selection(); net::Client *client = m_owner.client(); - if(sel && sel->pasteOrMoveToCanvas(m_messages, client->myId(), - m_owner.activeLayer(), m_owner.selectInterpolation(), - client->isCompatibilityMode())) { - m_owner.client()->sendMessages(m_messages.count(), m_messages.constData()); + if(sel && + sel->pasteOrMoveToCanvas( + m_messages, client->myId(), m_owner.activeLayer(), + m_owner.selectInterpolation(), client->isCompatibilityMode())) { + m_owner.client()->sendMessages( + m_messages.count(), m_messages.constData()); m_messages.clear(); m_owner.model()->setSelection(nullptr); } @@ -156,7 +160,8 @@ void SelectionTool::startMove() canvas::Selection *sel = model->selection(); Q_ASSERT(sel); - // Get the selection shape mask (needs to be done before the shape is overwritten by setMoveImage) + // Get the selection shape mask (needs to be done before the shape is + // overwritten by setMoveImage) QRect maskBounds; QImage eraseMask = sel->shapeMask(Qt::white, &maskBounds); @@ -165,13 +170,15 @@ void SelectionTool::startMove() const QImage img = model->selectionToImage(layerId); sel->setMoveImage(img, maskBounds, model->size(), layerId); - // The actual canvas pixels aren't touch yet, so we create a temporary sublayer - // to erase the selected region. + // The actual canvas pixels aren't touch yet, so we create a temporary + // sublayer to erase the selected region. model->paintEngine()->previewCut(layerId, maskBounds, eraseMask); } RectangleSelection::RectangleSelection(ToolController &owner) - : SelectionTool(owner, SELECTION, QCursor(QPixmap(":cursors/select-rectangle.png"), 2, 2)) + : SelectionTool( + owner, SELECTION, + QCursor(QPixmap(":cursors/select-rectangle.png"), 2, 2)) { } @@ -181,7 +188,8 @@ void RectangleSelection::initSelection(canvas::Selection *selection) selection->setShapeRect(QRect(p, p)); } -void RectangleSelection::newSelectionMotion(const canvas::Point &point, bool constrain, bool center) +void RectangleSelection::newSelectionMotion( + const canvas::Point &point, bool constrain, bool center) { QPointF p; if(constrain) @@ -194,20 +202,24 @@ void RectangleSelection::newSelectionMotion(const canvas::Point &point, bool con else m_p1 = m_start; - m_owner.model()->selection()->setShapeRect(QRectF(m_p1, p).normalized().toRect()); + m_owner.model()->selection()->setShapeRect( + QRectF(m_p1, p).normalized().toRect()); } PolygonSelection::PolygonSelection(ToolController &owner) - : SelectionTool(owner, POLYGONSELECTION, QCursor(QPixmap(":cursors/select-lasso.png"), 2, 29)) + : SelectionTool( + owner, POLYGONSELECTION, + QCursor(QPixmap(":cursors/select-lasso.png"), 2, 29)) { } void PolygonSelection::initSelection(canvas::Selection *selection) { - selection->setShape(QPolygonF({ m_start })); + selection->setShape(QPolygonF({m_start})); } -void PolygonSelection::newSelectionMotion(const canvas::Point &point, bool constrain, bool center) +void PolygonSelection::newSelectionMotion( + const canvas::Point &point, bool constrain, bool center) { Q_UNUSED(constrain); Q_UNUSED(center); @@ -216,7 +228,8 @@ void PolygonSelection::newSelectionMotion(const canvas::Point &point, bool const m_owner.model()->selection()->addPointToShape(point); } -QImage SelectionTool::transformSelectionImage(const QImage &source, const QPolygon &target, QPoint *offset) +QImage SelectionTool::transformSelectionImage( + const QImage &source, const QPolygon &target, QPoint *offset) { Q_ASSERT(!source.isNull()); Q_ASSERT(target.size() == 4); @@ -243,18 +256,18 @@ QImage SelectionTool::transformSelectionImage(const QImage &source, const QPolyg return out; } -QPolygon SelectionTool::destinationQuad(const QImage &source, const QPolygon &target, QRect *outBounds, QPolygonF *outSrcPolygon) +QPolygon SelectionTool::destinationQuad( + const QImage &source, const QPolygon &target, QRect *outBounds, + QPolygonF *outSrcPolygon) { Q_ASSERT(!source.isNull()); Q_ASSERT(target.size() == 4); const QRect bounds = target.boundingRect(); - const QPolygonF srcPolygon({ - QPointF(0, 0), - QPointF(source.width(), 0), - QPointF(source.width(), source.height()), - QPointF(0, source.height()) - }); + const QPolygonF srcPolygon( + {QPointF(0, 0), QPointF(source.width(), 0), + QPointF(source.width(), source.height()), + QPointF(0, source.height())}); if(outBounds) { *outBounds = bounds; @@ -265,13 +278,17 @@ QPolygon SelectionTool::destinationQuad(const QImage &source, const QPolygon &ta return target.translated(-bounds.topLeft()); } -QImage SelectionTool::shapeMask(const QColor &color, const QPolygonF &selection, QRect *maskBounds, bool mono) +QImage SelectionTool::shapeMask( + const QColor &color, const QPolygonF &selection, QRect *maskBounds, + bool mono) { const QRectF bf = selection.boundingRect(); const QRect b = bf.toRect(); const QPolygonF p = selection.translated(-bf.topLeft()); - QImage mask(b.size(), mono ? QImage::Format_Mono : QImage::Format_ARGB32_Premultiplied); + QImage mask( + b.size(), + mono ? QImage::Format_Mono : QImage::Format_ARGB32_Premultiplied); mask.fill(0); QPainter painter(&mask); diff --git a/src/libclient/tools/selection.h b/src/libclient/tools/selection.h index ce336db284..ccad46a71f 100644 --- a/src/libclient/tools/selection.h +++ b/src/libclient/tools/selection.h @@ -1,10 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-or-later - #ifndef TOOLS_SELECTION_H #define TOOLS_SELECTION_H - #include "libclient/canvas/selection.h" -#include "libclient/drawdance/message.h" +#include "libclient/net/message.h" #include "libclient/tools/tool.h" class QImage; @@ -23,10 +21,15 @@ class SelectionTool : public Tool { SelectionTool(ToolController &owner, Type type, QCursor cursor) : Tool(owner, type, cursor, true, false, false) , m_allowTransform{true} - {} + { + } + + void + begin(const canvas::Point &point, bool right, float zoom) override final; + + void motion( + const canvas::Point &point, bool constrain, bool center) override final; - void begin(const canvas::Point& point, bool right, float zoom) override final; - void motion(const canvas::Point& point, bool constrain, bool center) override final; void end() override final; void finishMultipart() override final; @@ -42,22 +45,26 @@ class SelectionTool : public Tool { //! Allow selection moving and resizing void setTransformEnabled(bool enable) { m_allowTransform = enable; } - static QImage transformSelectionImage(const QImage &source, const QPolygon &target, QPoint *offset); + static QImage transformSelectionImage( + const QImage &source, const QPolygon &target, QPoint *offset); static QPolygon destinationQuad( const QImage &source, const QPolygon &target, QRect *outBounds = nullptr, QPolygonF *outSrcPolygon = nullptr); - static QImage shapeMask(const QColor &color, const QPolygonF &selection, QRect *maskBounds, bool mono); + static QImage shapeMask( + const QColor &color, const QPolygonF &selection, QRect *maskBounds, + bool mono); protected: virtual void initSelection(canvas::Selection *selection) = 0; - virtual void newSelectionMotion(const canvas::Point &point, bool constrain, bool center) = 0; + virtual void newSelectionMotion( + const canvas::Point &point, bool constrain, bool center) = 0; QPointF m_start, m_p1, m_end; canvas::Selection::Handle m_handle; private: bool m_allowTransform; - drawdance::MessageList m_messages; + net::MessageList m_messages; }; class RectangleSelection final : public SelectionTool { @@ -66,7 +73,8 @@ class RectangleSelection final : public SelectionTool { protected: void initSelection(canvas::Selection *selection) override; - void newSelectionMotion(const canvas::Point &point, bool constrain, bool center) override; + void newSelectionMotion( + const canvas::Point &point, bool constrain, bool center) override; }; class PolygonSelection final : public SelectionTool { @@ -75,10 +83,10 @@ class PolygonSelection final : public SelectionTool { protected: void initSelection(canvas::Selection *selection) override; - void newSelectionMotion(const canvas::Point &point, bool constrain, bool center) override; + void newSelectionMotion( + const canvas::Point &point, bool constrain, bool center) override; }; } #endif - diff --git a/src/libserver/client.cpp b/src/libserver/client.cpp index 55af3fa20b..38bea8bd1b 100644 --- a/src/libserver/client.cpp +++ b/src/libserver/client.cpp @@ -1,31 +1,26 @@ // SPDX-License-Identifier: GPL-3.0-or-later #include "libserver/client.h" +#include "libserver/serverconfig.h" +#include "libserver/serverlog.h" #include "libserver/session.h" #include "libserver/sessionhistory.h" -#include "libserver/serverlog.h" -#include "libserver/serverconfig.h" - #include "libshared/net/messagequeue.h" -#include "libshared/net/control.h" -#include "libshared/net/meta.h" +#include "libshared/net/servercmd.h" #include "libshared/util/qtcompat.h" - +#include #include #include -#include namespace server { -using protocol::MessagePtr; - struct Client::Private { QPointer session; QTcpSocket *socket; ServerLog *logger; - protocol::MessageQueue *msgqueue; - protocol::MessageList holdqueue; + net::MessageQueue *msgqueue; + net::MessageList holdqueue; QString username; QString authId; @@ -44,7 +39,8 @@ struct Client::Private { bool isAwaitingReset = false; Private(QTcpSocket *socket_, ServerLog *logger_) - : socket(socket_), logger(logger_) + : socket(socket_) + , logger(logger_) { Q_ASSERT(socket); Q_ASSERT(logger); @@ -52,15 +48,21 @@ struct Client::Private { }; Client::Client(QTcpSocket *socket, ServerLog *logger, QObject *parent) - : QObject(parent), d(new Private(socket, logger)) + : QObject(parent) + , d(new Private(socket, logger)) { - d->msgqueue = new protocol::MessageQueue(socket, this); + d->msgqueue = new net::MessageQueue(socket, false, this); d->socket->setParent(this); - connect(d->socket, &QAbstractSocket::disconnected, this, &Client::socketDisconnect); + connect( + d->socket, &QAbstractSocket::disconnected, this, + &Client::socketDisconnect); connect(d->socket, compat::SocketError, this, &Client::socketError); - connect(d->msgqueue, &protocol::MessageQueue::messageAvailable, this, &Client::receiveMessages); - connect(d->msgqueue, &protocol::MessageQueue::badData, this, &Client::gotBadData); + connect( + d->msgqueue, &net::MessageQueue::messageAvailable, this, + &Client::receiveMessages); + connect( + d->msgqueue, &net::MessageQueue::badData, this, &Client::gotBadData); } Client::~Client() @@ -68,19 +70,16 @@ Client::~Client() delete d; } -protocol::MessageQueue *Client::messageQueue() +net::MessageQueue *Client::messageQueue() { return d->msgqueue; } -protocol::MessagePtr Client::joinMessage() const +net::Message Client::joinMessage() const { - return protocol::MessagePtr(new protocol::UserJoin( - id(), - (isAuthenticated() ? protocol::UserJoin::FLAG_AUTH : 0) | (isModerator() ? protocol::UserJoin::FLAG_MOD : 0), - username(), - avatar() - )); + uint8_t flags = (isAuthenticated() ? DP_MSG_JOIN_FLAGS_AUTH : 0) | + (isModerator() ? DP_MSG_JOIN_FLAGS_MOD : 0); + return net::makeJoinMessage(id(), flags, username(), avatar()); } QJsonObject Client::description(bool includeSession) const @@ -89,21 +88,25 @@ QJsonObject Client::description(bool includeSession) const u["id"] = id(); u["name"] = username(); u["ip"] = peerAddress().toString(); - u["lastActive"] = QDateTime::fromMSecsSinceEpoch(d->lastActive, Qt::UTC).toString(Qt::ISODate); + u["lastActive"] = QDateTime::fromMSecsSinceEpoch(d->lastActive, Qt::UTC) + .toString(Qt::ISODate); u["auth"] = isAuthenticated(); u["op"] = isOperator(); u["muted"] = isMuted(); u["mod"] = isModerator(); u["tls"] = isSecure(); - if(includeSession && d->session) + if(includeSession && d->session) { u["session"] = d->session->id(); + } return u; } -JsonApiResult Client::callJsonApi(JsonApiMethod method, const QStringList &path, const QJsonObject &request) +JsonApiResult Client::callJsonApi( + JsonApiMethod method, const QStringList &path, const QJsonObject &request) { - if(!path.isEmpty()) + if(!path.isEmpty()) { return JsonApiNotFound(); + } if(method == JsonApiMethod::Delete) { disconnectClient(Client::DisconnectionReason::Kick, "server operator"); @@ -113,31 +116,35 @@ JsonApiResult Client::callJsonApi(JsonApiMethod method, const QStringList &path, } else if(method == JsonApiMethod::Update) { QString msg = request["message"].toString(); - if(!msg.isEmpty()) + if(!msg.isEmpty()) { sendSystemChat(msg); + } QString alert = request["alert"].toString(); - if(!alert.isEmpty()) + if(!alert.isEmpty()) { sendSystemChat(alert, true); + } if(request.contains("op")) { const bool op = request["op"].toBool(); if(d->isOperator != op && d->session) { - d->session->changeOpStatus(id(), op, "the server administrator"); + d->session->changeOpStatus( + id(), op, "the server administrator"); } } if(request.contains("trusted")) { const bool trusted = request["trusted"].toBool(); if(d->isTrusted != trusted && d->session) { - d->session->changeTrustedStatus(id(), trusted, "the server administrator"); + d->session->changeTrustedStatus( + id(), trusted, "the server administrator"); } } - return JsonApiResult { JsonApiResult::Ok, QJsonDocument(description()) }; + return JsonApiResult{JsonApiResult::Ok, QJsonDocument(description())}; } else if(method == JsonApiMethod::Get) { - return JsonApiResult { JsonApiResult::Ok, QJsonDocument(description()) }; + return JsonApiResult{JsonApiResult::Ok, QJsonDocument(description())}; } else { return JsonApiBadMethod(); @@ -156,7 +163,7 @@ Session *Client::session() void Client::setId(uint8_t id) { - Q_ASSERT(d->id==0 && id != 0); // ID is only assigned once + Q_ASSERT(d->id == 0 && id != 0); // ID is only assigned once d->id = id; } @@ -217,10 +224,8 @@ bool Client::isOperator() const bool Client::isDeputy() const { - return !isOperator() - && isTrusted() - && d->session - && d->session->history()->hasFlag(SessionHistory::Deputies); + return !isOperator() && isTrusted() && d->session && + d->session->history()->hasFlag(SessionHistory::Deputies); } void Client::setModerator(bool mod) @@ -245,7 +250,7 @@ void Client::setTrusted(bool trusted) bool Client::isAuthenticated() const { - return! d->authId.isEmpty(); + return !d->authId.isEmpty(); } void Client::setMuted(bool m) @@ -263,88 +268,86 @@ void Client::setConnectionTimeout(int timeout) d->msgqueue->setIdleTimeout(timeout); } -#ifndef NDEBUG -void Client::setRandomLag(uint lag) -{ - d->msgqueue->setRandomLag(lag); -} -#endif - QHostAddress Client::peerAddress() const { return d->socket->peerAddress(); } -void Client::sendDirectMessage(protocol::MessagePtr msg) +void Client::sendDirectMessage(const net::Message &msg) { - if(!d->isAwaitingReset || msg->isControl()) + if(!d->isAwaitingReset || msg.isControl()) { d->msgqueue->send(msg); + } } -void Client::sendDirectMessage(const protocol::MessageList &msgs) +void Client::sendDirectMessages(const net::MessageList &msgs) { if(d->isAwaitingReset) { - for(MessagePtr msg : msgs) - if(msg->isControl()) + for(const net::Message &msg : msgs) { + if(msg.isControl()) { d->msgqueue->send(msg); + } + } } else { - d->msgqueue->send(msgs); + d->msgqueue->sendMultiple(msgs.size(), msgs.constData()); } } void Client::sendSystemChat(const QString &message, bool alert) { - protocol::ServerReply msg { - alert ? protocol::ServerReply::ALERT : protocol::ServerReply::MESSAGE, - message, - QJsonObject() - }; - - d->msgqueue->send(MessagePtr(new protocol::Command(0, msg.toJson()))); + d->msgqueue->send( + alert ? net::ServerReply::makeAlert(message) + : net::ServerReply::makeMessage(message)); } void Client::receiveMessages() { while(d->msgqueue->isPending()) { - MessagePtr msg = d->msgqueue->getPending(); - + net::Message msg = d->msgqueue->shiftPending(); d->lastActive = QDateTime::currentMSecsSinceEpoch(); if(d->session.isNull()) { // No session? We must be in the login phase - if(msg->type() == protocol::MSG_COMMAND) + if(msg.type() == DP_MSG_SERVER_COMMAND) { emit loginMessage(msg); - else - log(Log().about(Log::Level::Warn, Log::Topic::RuleBreak).message( - QString("Got non-login message (type=%1) in login state").arg(msg->type()) - )); - + } else { + log(Log() + .about(Log::Level::Warn, Log::Topic::RuleBreak) + .message(QStringLiteral("Got non-login message " + "(type=%1) in login state") + .arg(msg.type()))); + } } else { - // Enforce origin ID, except when receiving a snapshot - if(d->session->initUserId() != d->id) - msg->setContextId(d->id); + if(d->session->initUserId() != d->id) { + msg.setContextId(d->id); + } - if(isHoldLocked()) - d->holdqueue << msg; - else + if(isHoldLocked()) { + d->holdqueue.append(msg); + } else { d->session->handleClientMessage(*this, msg); + } } } } void Client::gotBadData(int len, int type) { - log(Log().about(Log::Level::Warn, Log::Topic::RuleBreak).message( - QString("Received unknown message type %1 of length %2").arg(type).arg(len) - )); + log(Log() + .about(Log::Level::Warn, Log::Topic::RuleBreak) + .message(QString("Received unknown message type %1 of length %2") + .arg(type) + .arg(len))); d->socket->abort(); } void Client::socketError(QAbstractSocket::SocketError error) { if(error != QAbstractSocket::RemoteHostClosedError) { - log(Log().about(Log::Level::Warn, Log::Topic::Status).message("Socket error: " + d->socket->errorString())); + log(Log() + .about(Log::Level::Warn, Log::Topic::Status) + .message("Socket error: " + d->socket->errorString())); d->socket->abort(); } } @@ -355,22 +358,25 @@ void Client::socketDisconnect() this->deleteLater(); } -void Client::disconnectClient(DisconnectionReason reason, const QString &message) +void Client::disconnectClient( + DisconnectionReason reason, const QString &message) { - protocol::Disconnect::Reason pr { protocol::Disconnect::OTHER }; - Log::Topic topic { Log::Topic::Leave }; + net::MessageQueue::GracefulDisconnect pr{ + net::MessageQueue::GracefulDisconnect::Other}; + Log::Topic topic{Log::Topic::Leave}; switch(reason) { case DisconnectionReason::Kick: - pr = protocol::Disconnect::KICK; + pr = net::MessageQueue::GracefulDisconnect::Kick; topic = Log::Topic::Kick; break; - case DisconnectionReason::Error: pr = protocol::Disconnect::ERROR; break; - case DisconnectionReason::Shutdown: pr = protocol::Disconnect::SHUTDOWN; break; + case DisconnectionReason::Error: + pr = net::MessageQueue::GracefulDisconnect::Error; + break; + case DisconnectionReason::Shutdown: + pr = net::MessageQueue::GracefulDisconnect::Shutdown; + break; } - log(Log() - .about(Log::Level::Info, topic) - .message(message) - ); + log(Log().about(Log::Level::Info, topic).message(message)); emit loggedOff(this); d->msgqueue->sendDisconnect(pr, message); @@ -380,8 +386,9 @@ void Client::setHoldLocked(bool lock) { d->isHoldLocked = lock; if(!lock) { - for(MessagePtr msg : d->holdqueue) + for(const net::Message &msg : d->holdqueue) { d->session->handleClientMessage(*this, msg); + } d->holdqueue.clear(); } } @@ -408,13 +415,13 @@ bool Client::hasSslSupport() const bool Client::isSecure() const { - QSslSocket *socket = qobject_cast(d->socket); + QSslSocket *socket = qobject_cast(d->socket); return socket && socket->isEncrypted(); } void Client::startTls() { - QSslSocket *socket = qobject_cast(d->socket); + QSslSocket *socket = qobject_cast(d->socket); Q_ASSERT(socket); socket->startServerEncryption(); } @@ -428,10 +435,10 @@ void Client::log(Log entry) const d->logger->logMessage(entry); } -Log Client::log() const { +Log Client::log() const +{ return Log().user(d->id, d->socket->peerAddress(), d->username); } } - diff --git a/src/libserver/client.h b/src/libserver/client.h index b1318cdeb8..62972b4ded 100644 --- a/src/libserver/client.h +++ b/src/libserver/client.h @@ -1,18 +1,15 @@ // SPDX-License-Identifier: GPL-3.0-or-later - #ifndef DP_SERVER_CLIENT_H #define DP_SERVER_CLIENT_H - -#include "libshared/net/message.h" #include "libserver/jsonapi.h" - +#include "libshared/net/message.h" #include #include class QHostAddress; -namespace protocol { - class MessageQueue; +namespace net { +class MessageQueue; } namespace server { @@ -28,9 +25,8 @@ class ServerLog; * A client is initially in a "lobby" state, until it finishes the login * handshake, at which point it is assigned to a session. */ -class Client : public QObject -{ - Q_OBJECT +class Client : public QObject { + Q_OBJECT public: ~Client() override; @@ -60,7 +56,8 @@ class Client : public QObject /** * @brief Get an authenticated user's flags * - * User flags come from the account system, either the built-in one or ext-auth. + * User flags come from the account system, either the built-in one or + * ext-auth. */ QStringList authFlags() const; void setAuthFlags(const QStringList &flags); @@ -108,7 +105,8 @@ class Client : public QObject /** * @brief Is this user a moderator? - * Moderators can access any session, always have OP status and cannot be kicked by other users. + * Moderators can access any session, always have OP status and cannot be + * kicked by other users. */ bool isModerator() const; void setModerator(bool mod); @@ -116,14 +114,16 @@ class Client : public QObject /** * @brief Is this a trusted user? * - * The trust flag is granted by session operators. It's effects are purely clientside, - * but the server is aware of it so it can remember it for authenticated users. + * The trust flag is granted by session operators. It's effects are purely + * clientside, but the server is aware of it so it can remember it for + * authenticated users. */ bool isTrusted() const; void setTrusted(bool trusted); /** - * @brief Has this user been authenticated (using either an internal account or ext-auth)? + * @brief Has this user been authenticated (using either an internal account + * or ext-auth)? */ bool isAuthenticated() const; @@ -140,19 +140,16 @@ class Client : public QObject void setConnectionTimeout(int timeout); /** - * Get the timestamp of this client's last activity (i.e. non-keepalive message received) + * Get the timestamp of this client's last activity (i.e. non-keepalive + * message received) * * Returned value is given in milliseconds since Epoch. */ qint64 lastActive() const; -#ifndef NDEBUG - void setRandomLag(uint lag); -#endif - enum class DisconnectionReason { - Kick, // kicked by an operator - Error, // kicked due to some server or protocol error + Kick, // kicked by an operator + Error, // kicked due to some server or protocol error Shutdown, // the server is shutting down }; @@ -171,11 +168,12 @@ class Client : public QObject * @brief Send a message directly to this client * * Note. Typically messages are sent via the shared session history. Direct - * messages are used during the login phase and for client specific notifications. + * messages are used during the login phase and for client specific + * notifications. * @param msg */ - void sendDirectMessage(protocol::MessagePtr msg); - void sendDirectMessage(const protocol::MessageList &msgs); + void sendDirectMessage(const net::Message &msg); + void sendDirectMessages(const net::MessageList &msgs); /** * @brief Send a message from the server directly to this user @@ -206,14 +204,14 @@ class Client : public QObject /** * @brief Get a Join message for this user */ - protocol::MessagePtr joinMessage() const; + net::Message joinMessage() const; /** * @brief Get a JSON object describing this user * * This is used by the admin API */ - QJsonObject description(bool includeSession=true) const; + QJsonObject description(bool includeSession = true) const; /** * @brief Call the client's JSON administration API @@ -225,7 +223,9 @@ class Client : public QObject * @param request request body content * @return JSON API response content */ - JsonApiResult callJsonApi(JsonApiMethod method, const QStringList &path, const QJsonObject &request); + JsonApiResult callJsonApi( + JsonApiMethod method, const QStringList &path, + const QJsonObject &request); /** * @brief Divert incoming messages to a holding buffer @@ -241,8 +241,8 @@ class Client : public QObject /** * @brief Block all messages sent to this user * - * This state is set when a fresh reset is imminent and we don't want to send - * any messages to the client before that happens. + * This state is set when a fresh reset is imminent and we don't want to + * send any messages to the client before that happens. */ void setAwaitingReset(bool awaiting); bool isAwaitingReset() const; @@ -260,7 +260,7 @@ class Client : public QObject /** * @brief Message received while not part of a session */ - void loginMessage(protocol::MessagePtr message); + void loginMessage(net::Message message); /** * @brief This client is disconnecting @@ -279,10 +279,10 @@ private slots: protected: Client(QTcpSocket *socket, ServerLog *logger, QObject *parent); - protocol::MessageQueue *messageQueue(); + net::MessageQueue *messageQueue(); private: - void handleSessionMessage(protocol::MessagePtr msg); + void handleSessionMessage(net::Message msg); struct Private; Private *d; @@ -291,4 +291,3 @@ private slots: } #endif - diff --git a/src/libserver/filedhistory.cpp b/src/libserver/filedhistory.cpp index 368fc72e11..a8c3533f2c 100644 --- a/src/libserver/filedhistory.cpp +++ b/src/libserver/filedhistory.cpp @@ -1,34 +1,43 @@ // SPDX-License-Identifier: GPL-3.0-or-later - +extern "C" { +#include +#include +#include +#include +#include +#include +} #include "libserver/filedhistory.h" -#include "libshared/util/passwordhash.h" #include "libshared/util/filename.h" -#include "libshared/record/header.h" -#include "libshared/net/meta.h" - -#include -#include -#include +#include "libshared/util/passwordhash.h" #include +#include +#include #include +#include namespace server { // A block is closed when its size goes above this limit static const qint64 MAX_BLOCK_SIZE = 0xffff * 10; -FiledHistory::FiledHistory(const QDir &dir, QFile *journal, const QString &id, const QString &alias, const protocol::ProtocolVersion &version, const QString &founder, QObject *parent) - : SessionHistory(id, parent), - m_dir(dir), - m_journal(journal), - m_recording(nullptr), - m_alias(alias), - m_founder(founder), - m_version(version), - m_maxUsers(254), - m_flags(), - m_fileCount(0), - m_archive(false) +FiledHistory::FiledHistory( + const QDir &dir, QFile *journal, const QString &id, const QString &alias, + const protocol::ProtocolVersion &version, const QString &founder, + QObject *parent) + : SessionHistory(id, parent) + , m_dir(dir) + , m_journal(journal) + , m_recording(nullptr) + , m_reader(nullptr) + , m_writer(nullptr) + , m_alias(alias) + , m_founder(founder) + , m_version(version) + , m_maxUsers(254) + , m_flags() + , m_fileCount(0) + , m_archive(false) { Q_ASSERT(journal); @@ -36,13 +45,18 @@ FiledHistory::FiledHistory(const QDir &dir, QFile *journal, const QString &id, c startTimer(1000 * 30, Qt::VeryCoarseTimer); } -FiledHistory::FiledHistory(const QDir &dir, QFile *journal, const QString &id, QObject *parent) - : FiledHistory(dir, journal, id, QString(), protocol::ProtocolVersion(), QString(), parent) +FiledHistory::FiledHistory( + const QDir &dir, QFile *journal, const QString &id, QObject *parent) + : FiledHistory( + dir, journal, id, QString(), protocol::ProtocolVersion(), QString(), + parent) { } FiledHistory::~FiledHistory() { + DP_binary_writer_free(m_writer); + DP_binary_reader_free(m_reader); } QString FiledHistory::journalFilename(const QString &id) @@ -50,7 +64,8 @@ QString FiledHistory::journalFilename(const QString &id) return id + ".session"; } -static QString uniqueRecordingFilename(const QDir &dir, const QString &id, int idx) +static QString +uniqueRecordingFilename(const QDir &dir, const QString &id, int idx) { QString idstr = id; if(idx > 1) @@ -60,11 +75,16 @@ static QString uniqueRecordingFilename(const QDir &dir, const QString &id, int i return utils::uniqueFilename(dir, idstr, "dprec", false); } -FiledHistory *FiledHistory::startNew(const QDir &dir, const QString &id, const QString &alias, const protocol::ProtocolVersion &version, const QString &founder, QObject *parent) +FiledHistory *FiledHistory::startNew( + const QDir &dir, const QString &id, const QString &alias, + const protocol::ProtocolVersion &version, const QString &founder, + QObject *parent) { - QFile *journal = new QFile(QFileInfo(dir, journalFilename(id)).absoluteFilePath()); + QFile *journal = + new QFile(QFileInfo(dir, journalFilename(id)).absoluteFilePath()); - FiledHistory *fh = new FiledHistory(dir, journal, id, alias, version, founder, parent); + FiledHistory *fh = + new FiledHistory(dir, journal, id, alias, version, founder, parent); journal->setParent(fh); if(!fh->create()) { @@ -125,7 +145,8 @@ bool FiledHistory::initRecording() { Q_ASSERT(m_blocks.isEmpty()); - const QString filename = uniqueRecordingFilename(m_dir, id(), ++m_fileCount); + const QString filename = + uniqueRecordingFilename(m_dir, id(), ++m_fileCount); m_recording = new QFile(m_dir.absoluteFilePath(filename), this); if(!m_recording->open(QFile::ReadWrite)) { @@ -133,22 +154,33 @@ bool FiledHistory::initRecording() return false; } - QJsonObject metadata; - metadata["version"] = m_version.asString(); // the hosting client's protocol version - recording::writeRecordingHeader(m_recording, metadata); + Q_ASSERT(!m_reader); + m_reader = DP_binary_reader_new( + DP_qfile_input_new(m_recording, false, DP_input_new), + DP_BINARY_READER_FLAG_NO_LENGTH | DP_BINARY_READER_FLAG_NO_HEADER); + Q_ASSERT(!m_writer); + m_writer = DP_binary_writer_new( + DP_qfile_output_new(m_recording, false, DP_output_new)); + + JSON_Value *header_value = json_value_init_object(); + JSON_Object *header_object = json_value_get_object(header_value); + json_object_set_string( // the hosting client's protocol version + header_object, "version", qUtf8Printable(m_version.asString())); + bool ok = DP_binary_writer_write_header(m_writer, header_object); + json_value_free(header_value); + if(!ok) { + qWarning() << filename << DP_error(); + return false; + } m_recording->flush(); m_journal->write(QString("FILE %1\n").arg(filename).toUtf8()); m_journal->flush(); - m_blocks << Block { - m_recording->pos(), - firstIndex(), - 0, - m_recording->pos(), - protocol::MessageList() - }; + m_blocks << Block{ + m_recording->pos(), firstIndex(), 0, m_recording->pos(), + net::MessageList()}; return true; } @@ -165,15 +197,15 @@ bool FiledHistory::load() if(line.isEmpty() || line.at(0) == '#') continue; - //qDebug() << QString::fromUtf8(line.trimmed()); + // qDebug() << QString::fromUtf8(line.trimmed()); QByteArray params; int sep = line.indexOf(' '); - if(sep<0) { + if(sep < 0) { cmd = line; } else { cmd = line.left(sep).trimmed(); - params = line.mid(sep+1).trimmed(); + params = line.mid(sep + 1).trimmed(); } if(cmd == "FILE") { @@ -224,20 +256,25 @@ bool FiledHistory::load() else if(f == "authonly") flags |= AuthOnly; else - qWarning() << id() << "unknown flag:" << QString::fromUtf8(f); + qWarning() + << id() << "unknown flag:" << QString::fromUtf8(f); } m_flags = flags; } else if(cmd == "BAN") { const QList args = params.split(' '); if(args.length() != 5) { - qWarning() << id() << "invalid ban entry:" << QString::fromUtf8(params); + qWarning() << id() + << "invalid ban entry:" << QString::fromUtf8(params); } else { int id = args.at(0).toInt(); - QString name { QString::fromUtf8(QByteArray::fromPercentEncoding(args.at(1))) }; - QHostAddress ip { QString::fromUtf8(args.at(2)) }; - QString extAuthId { QString::fromUtf8(QByteArray::fromPercentEncoding(args.at(3))) }; - QString bannedBy { QString::fromUtf8(QByteArray::fromPercentEncoding(args.at(4))) }; + QString name{QString::fromUtf8( + QByteArray::fromPercentEncoding(args.at(1)))}; + QHostAddress ip{QString::fromUtf8(args.at(2))}; + QString extAuthId{QString::fromUtf8( + QByteArray::fromPercentEncoding(args.at(3)))}; + QString bannedBy{QString::fromUtf8( + QByteArray::fromPercentEncoding(args.at(4)))}; m_banlist.addBan(name, ip, extAuthId, bannedBy, id); } @@ -253,11 +290,13 @@ bool FiledHistory::load() } else if(cmd == "USER") { const QList args = params.split(' '); - if(args.length()!=2) { - qWarning() << "Invalid USER entry:" << QString::fromUtf8(params); + if(args.length() != 2) { + qWarning() << "Invalid USER entry:" + << QString::fromUtf8(params); } else { int id = args.at(0).toInt(); - QString name { QString::fromUtf8(QByteArray::fromPercentEncoding(args.at(1))) }; + QString name{QString::fromUtf8( + QByteArray::fromPercentEncoding(args.at(1)))}; idQueue().setIdForName(id, name); } @@ -278,7 +317,8 @@ bool FiledHistory::load() m_trusted.remove(QString::fromUtf8(params)); } else { - qWarning() << id() << "unknown journal entry:" << QString::fromUtf8(cmd); + qWarning() << id() + << "unknown journal entry:" << QString::fromUtf8(cmd); } } while(!m_journal->atEnd()); @@ -300,25 +340,34 @@ bool FiledHistory::load() return false; } - // Recording must have a valid header - QJsonObject header = recording::readRecordingHeader(m_recording); - if(header.isEmpty()) { + m_reader = DP_binary_reader_new( + DP_qfile_input_new(m_recording, false, DP_input_new), + DP_BINARY_READER_FLAG_NO_LENGTH); + if(!m_reader) { qWarning() << recordingFile << "invalid header"; return false; } - m_version = protocol::ProtocolVersion::fromString(header["version"].toString()); + JSON_Object *header = + json_value_get_object(DP_binary_reader_header(m_reader)); + + m_version = protocol::ProtocolVersion::fromString( + QString::fromUtf8(json_object_get_string(header, "version"))); if(!m_version.isValid()) { qWarning() << recordingFile << "invalid protocol version"; return false; } - if(m_version.serverVersion() != protocol::ProtocolVersion::current().serverVersion()) { + if(m_version.serverVersion() != + protocol::ProtocolVersion::current().serverVersion()) { qWarning() << recordingFile << "incompatible server version"; return false; } qint64 startOffset = m_recording->pos(); + Q_ASSERT(!m_writer); + m_writer = DP_binary_writer_new( + DP_qfile_output_new(m_recording, false, DP_output_new)); // Scan the recording file and build the index of blocks if(!scanBlocks()) { @@ -326,7 +375,9 @@ bool FiledHistory::load() return false; } - historyLoaded(m_blocks.last().endOffset-startOffset, m_blocks.last().startIndex+m_blocks.last().count); + historyLoaded( + m_blocks.last().endOffset - startOffset, + m_blocks.last().startIndex + m_blocks.last().count); // If a loaded session is empty, the server expects the first joining client // to supply the initial content, while the client is expecting to join @@ -344,25 +395,21 @@ bool FiledHistory::scanBlocks() Q_ASSERT(m_blocks.isEmpty()); // Note: m_recording should be at the start of the recording - m_blocks << Block { - m_recording->pos(), - firstIndex(), - 0, - m_recording->pos(), - protocol::MessageList() - }; + m_blocks << Block{ + m_recording->pos(), firstIndex(), 0, m_recording->pos(), + net::MessageList()}; QSet users; do { Block &b = m_blocks.last(); uint8_t msgType, ctxId; - - const int msglen = recording::skipRecordingMessage(m_recording, &msgType, &ctxId); - if(msglen<0) { + int msglen = DP_binary_reader_skip_message(m_reader, &msgType, &ctxId); + if(msglen < 0) { // Truncated message encountered. // Rewind back to the end of the previous message - qWarning() << m_recording->fileName() << "Recording truncated at" << int(b.endOffset); + qWarning() << m_recording->fileName() << "Recording truncated at" + << int(b.endOffset); m_recording->seek(b.endOffset); break; } @@ -371,19 +418,17 @@ bool FiledHistory::scanBlocks() b.endOffset += msglen; Q_ASSERT(b.endOffset == m_recording->pos()); - if(b.endOffset-b.startOffset >= MAX_BLOCK_SIZE) { - m_blocks << Block { - b.endOffset, - b.startIndex+b.count, - 0, - b.endOffset, - protocol::MessageList() - }; + if(b.endOffset - b.startOffset >= MAX_BLOCK_SIZE) { + m_blocks << Block{ + b.endOffset, b.startIndex + b.count, 0, b.endOffset, + net::MessageList()}; } switch(msgType) { - case protocol::MSG_USER_JOIN: users.insert(ctxId); break; - case protocol::MSG_USER_LEAVE: + case DP_MSG_JOIN: + users.insert(ctxId); + break; + case DP_MSG_LEAVE: users.remove(ctxId); idQueue().reserveId(ctxId); break; @@ -392,12 +437,12 @@ bool FiledHistory::scanBlocks() // There should be no users at the end of the recording. for(const uint8_t user : users) { - protocol::UserLeave msg(user); + net::Message msg = net::makeLeaveMessage(user); m_blocks.last().count++; m_blocks.last().endOffset += msg.length(); - char buf[16]; - msg.serialize(buf); - m_recording->write(buf, msg.length()); + if(DP_binary_writer_write_message(m_writer, msg.get()) == 0) { + return false; + } idQueue().reserveId(user); } return true; @@ -405,6 +450,10 @@ bool FiledHistory::scanBlocks() void FiledHistory::terminate() { + DP_binary_reader_free(m_reader); + m_reader = nullptr; + DP_binary_writer_free(m_writer); + m_writer = nullptr; m_recording->close(); m_journal->close(); @@ -425,17 +474,13 @@ void FiledHistory::closeBlock() // Check if anything needs to be done Block &b = m_blocks.last(); - if(b.count==0) + if(b.count == 0) return; // Mark last block as closed and start a new one - m_blocks << Block { - b.endOffset, - b.startIndex+b.count, - 0, - b.endOffset, - protocol::MessageList() - }; + m_blocks << Block{ + b.endOffset, b.startIndex + b.count, 0, b.endOffset, + net::MessageList()}; } void FiledHistory::setPasswordHash(const QByteArray &password) @@ -474,7 +519,8 @@ void FiledHistory::setMaxUsers(int max) void FiledHistory::setAutoResetThreshold(uint limit) { - const uint newLimit = sizeLimit() == 0 ? limit : qMin(uint(sizeLimit() * 0.9), limit); + const uint newLimit = + sizeLimit() == 0 ? limit : qMin(uint(sizeLimit() * 0.9), limit); if(newLimit != m_autoResetThreshold) { m_autoResetThreshold = newLimit; m_journal->write(QString("AUTORESET %1\n").arg(newLimit).toUtf8()); @@ -515,82 +561,78 @@ void FiledHistory::joinUser(uint8_t id, const QString &name) { SessionHistory::joinUser(id, name); m_journal->write( - "USER " - + QByteArray::number(int(id)) - + " " - + name.toUtf8().toPercentEncoding(QByteArray(), " ") - + "\n"); + "USER " + QByteArray::number(int(id)) + " " + + name.toUtf8().toPercentEncoding(QByteArray(), " ") + "\n"); m_journal->flush(); } -std::tuple FiledHistory::getBatch(int after) const +std::tuple FiledHistory::getBatch(int after) const { // Find the block that contains the index *after* - int i=m_blocks.size()-1; - for(;i>0;--i) { - const Block &b = m_blocks.at(i-1); - if(b.startIndex+b.count-1 <= after) + int i = m_blocks.size() - 1; + for(; i > 0; --i) { + const Block &b = m_blocks.at(i - 1); + if(b.startIndex + b.count - 1 <= after) break; } - const Block &b = m_blocks.at(i); - - const int idxOffset = qMax(0, after - b.startIndex + 1); - if(idxOffset >= b.count) - return std::make_tuple(protocol::MessageList(), b.startIndex+b.count-1); + Block &b = m_blocks[i]; + int idxOffset = qMax(0, after - b.startIndex + 1); + if(idxOffset >= b.count) { + return std::make_tuple(net::MessageList(), b.startIndex + b.count - 1); + } - if(b.messages.isEmpty() && b.count>0) { + if(b.messages.isEmpty() && b.count > 0) { // Load the block worth of messages to memory if not already loaded const qint64 prevPos = m_recording->pos(); qDebug() << m_recording->fileName() << "loading block" << i; m_recording->seek(b.startOffset); - QByteArray buffer; - for(int m=0;mfileName() << "read error!"; m_recording->close(); break; } - protocol::NullableMessageRef msg = protocol::Message::deserialize(reinterpret_cast(buffer.constData()), buffer.length(), false); - if(msg.isNull()) { - qWarning() << m_recording->fileName() << "Invalid message in block" << i; - m_recording->close(); - break; - } - const_cast(b).messages << protocol::MessagePtr::fromNullable(msg); + b.messages.append(net::Message::noinc(msg)); } m_recording->seek(prevPos); } Q_ASSERT(b.messages.size() == b.count); - return std::make_tuple(b.messages.mid(idxOffset), b.startIndex+b.count-1); + return std::make_tuple( + b.messages.mid(idxOffset), b.startIndex + b.count - 1); } -void FiledHistory::historyAdd(const protocol::MessagePtr &msg) +void FiledHistory::historyAdd(const net::Message &msg) { - QVarLengthArray buf(msg->length()); - const int len = msg->serialize(buf.data()); - Q_ASSERT(len == buf.length()); - m_recording->write(buf.data(), len); + size_t len = DP_binary_writer_write_message(m_writer, msg.get()); Block &b = m_blocks.last(); b.count++; b.endOffset += len; - // Add message to cache, if already active (if cache is empty, it will be loaded from disk when needed) + // Add message to cache, if already active (if cache is empty, it will be + // loaded from disk when needed) if(!b.messages.isEmpty()) b.messages.append(msg); - if(b.endOffset-b.startOffset > MAX_BLOCK_SIZE) + if(b.endOffset - b.startOffset > MAX_BLOCK_SIZE) closeBlock(); } -void FiledHistory::historyReset(const protocol::MessageList &newHistory) +void FiledHistory::historyReset(const net::MessageList &newHistory) { QFile *oldRecording = m_recording; oldRecording->close(); m_recording = nullptr; + DP_binary_reader_free(m_reader); + m_reader = nullptr; + DP_binary_writer_free(m_writer); + m_writer = nullptr; m_blocks.clear(); initRecording(); @@ -602,31 +644,35 @@ void FiledHistory::historyReset(const protocol::MessageList &newHistory) oldRecording->remove(); delete oldRecording; - for(const protocol::MessagePtr &msg : newHistory) + for(const net::Message &msg : newHistory) { historyAdd(msg); + } } void FiledHistory::cleanupBatches(int before) { for(Block &b : m_blocks) { - if(b.startIndex+b.count >= before) + if(b.startIndex + b.count >= before) break; if(!b.messages.isEmpty()) { - qDebug() << "releasing history block cache from" << b.startIndex << "to" << b.startIndex+b.count-1; - b.messages = protocol::MessageList(); + qDebug() << "releasing history block cache from" << b.startIndex + << "to" << b.startIndex + b.count - 1; + b.messages = net::MessageList(); } } } -void FiledHistory::historyAddBan(int id, const QString &username, const QHostAddress &ip, const QString &extAuthId, const QString &bannedBy) +void FiledHistory::historyAddBan( + int id, const QString &username, const QHostAddress &ip, + const QString &extAuthId, const QString &bannedBy) { const QByteArray include = " "; - QByteArray entry = "BAN " + - QByteArray::number(id) + " " + - username.toUtf8().toPercentEncoding(QByteArray(), include) + " " + - ip.toString().toUtf8() + " " + - extAuthId.toUtf8().toPercentEncoding(QByteArray(), include) + " " + - bannedBy.toUtf8().toPercentEncoding(QByteArray(), include) + "\n"; + QByteArray entry = + "BAN " + QByteArray::number(id) + " " + + username.toUtf8().toPercentEncoding(QByteArray(), include) + " " + + ip.toString().toUtf8() + " " + + extAuthId.toUtf8().toPercentEncoding(QByteArray(), include) + " " + + bannedBy.toUtf8().toPercentEncoding(QByteArray(), include) + "\n"; m_journal->write(entry); m_journal->flush(); } @@ -695,7 +741,8 @@ void FiledHistory::setAuthenticatedTrust(const QString &authId, bool trusted) } else { if(m_trusted.contains(authId)) { m_trusted.remove(authId); - m_journal->write(QStringLiteral("UNTRUST %1\n").arg(authId).toUtf8()); + m_journal->write( + QStringLiteral("UNTRUST %1\n").arg(authId).toUtf8()); m_journal->flush(); } } diff --git a/src/libserver/filedhistory.h b/src/libserver/filedhistory.h index 5384c8a0f6..ed624cd449 100644 --- a/src/libserver/filedhistory.h +++ b/src/libserver/filedhistory.h @@ -5,15 +5,16 @@ #include "libserver/sessionhistory.h" #include "libshared/net/protover.h" - #include -#include #include +#include + +struct DP_BinaryReader; +struct DP_BinaryWriter; namespace server { -class FiledHistory final : public SessionHistory -{ +class FiledHistory final : public SessionHistory { Q_OBJECT public: ~FiledHistory() override; @@ -28,7 +29,10 @@ class FiledHistory final : public SessionHistory * @param parent * @return FiledHistory object or nullptr on error */ - static FiledHistory *startNew(const QDir &dir, const QString &id, const QString &alias, const protocol::ProtocolVersion &version, const QString &founder, QObject *parent=nullptr); + static FiledHistory *startNew( + const QDir &dir, const QString &id, const QString &alias, + const protocol::ProtocolVersion &version, const QString &founder, + QObject *parent = nullptr); /** * @brief Load a session from file @@ -36,7 +40,7 @@ class FiledHistory final : public SessionHistory * @param parent * @return */ - static FiledHistory *load(const QString &path, QObject *parent=nullptr); + static FiledHistory *load(const QString &path, QObject *parent = nullptr); /** * @brief Close the currently open block (if any) and start a new one @@ -47,7 +51,8 @@ class FiledHistory final : public SessionHistory * @brief Enable archival mode * * In archive mode, files are not deleted when session ends or is reset. - * Instead, ".archived" is appended to the end of the journal file on termination. + * Instead, ".archived" is appended to the end of the journal file on + * termination. * @param archive */ void setArchive(bool archive) { m_archive = archive; } @@ -57,7 +62,10 @@ class FiledHistory final : public SessionHistory QString idAlias() const override { return m_alias; } QString founderName() const override { return m_founder; } - protocol::ProtocolVersion protocolVersion() const override { return m_version; } + protocol::ProtocolVersion protocolVersion() const override + { + return m_version; + } QByteArray passwordHash() const override { return m_password; } QByteArray opwordHash() const override { return m_opword; } int maxUsers() const override { return m_maxUsers; } @@ -75,7 +83,7 @@ class FiledHistory final : public SessionHistory void terminate() override; void cleanupBatches(int before) override; - std::tuple getBatch(int after) const override; + std::tuple getBatch(int after) const override; void addAnnouncement(const QString &) override; void removeAnnouncement(const QString &url) override; @@ -83,30 +91,44 @@ class FiledHistory final : public SessionHistory void setAuthenticatedOperator(const QString &authId, bool op) override; void setAuthenticatedTrust(const QString &authId, bool trusted) override; - bool isOperator(const QString &authId) const override { return m_ops.contains(authId); } - bool isTrusted(const QString &authId) const override { return m_trusted.contains(authId); } + bool isOperator(const QString &authId) const override + { + return m_ops.contains(authId); + } + bool isTrusted(const QString &authId) const override + { + return m_trusted.contains(authId); + } bool isAuthenticatedOperators() const override { return !m_ops.isEmpty(); } protected: - void historyAdd(const protocol::MessagePtr &msg) override; - void historyReset(const protocol::MessageList &newHistory) override; - void historyAddBan(int id, const QString &username, const QHostAddress &ip, const QString &extAuthId, const QString &bannedBy) override; + void historyAdd(const net::Message &msg) override; + void historyReset(const net::MessageList &newHistory) override; + void historyAddBan( + int id, const QString &username, const QHostAddress &ip, + const QString &extAuthId, const QString &bannedBy) override; void historyRemoveBan(int id) override; void timerEvent(QTimerEvent *event) override; private: - FiledHistory(const QDir &dir, QFile *journal, const QString &id, const QString &alias, const protocol::ProtocolVersion &version, const QString &founder, QObject *parent); - FiledHistory(const QDir &dir, QFile *journal, const QString &id, QObject *parent); + FiledHistory( + const QDir &dir, QFile *journal, const QString &id, + const QString &alias, const protocol::ProtocolVersion &version, + const QString &founder, QObject *parent); + FiledHistory( + const QDir &dir, QFile *journal, const QString &id, QObject *parent); struct Block { qint64 startOffset; int startIndex; int count; qint64 endOffset; - protocol::MessageList messages; + net::MessageList messages; }; + void discardWriterOnError(const QString &context, const QString &filename); + bool create(); bool load(); bool scanBlocks(); @@ -115,6 +137,8 @@ class FiledHistory final : public SessionHistory QDir m_dir; QFile *m_journal; QFile *m_recording; + DP_BinaryReader *m_reader; + DP_BinaryWriter *m_writer; // Current state: QString m_alias; @@ -130,7 +154,7 @@ class FiledHistory final : public SessionHistory QSet m_ops; QSet m_trusted; - QVector m_blocks; + mutable QVector m_blocks; int m_fileCount; bool m_archive; }; diff --git a/src/libserver/inmemoryhistory.cpp b/src/libserver/inmemoryhistory.cpp index 73ff13f32b..0c5a5fbac6 100644 --- a/src/libserver/inmemoryhistory.cpp +++ b/src/libserver/inmemoryhistory.cpp @@ -1,41 +1,42 @@ // SPDX-License-Identifier: GPL-3.0-or-later - #include "libserver/inmemoryhistory.h" #include "libshared/util/passwordhash.h" namespace server { -InMemoryHistory::InMemoryHistory(const QString &id, const QString &alias, const protocol::ProtocolVersion &version, const QString &founder, QObject *parent) - : SessionHistory(id, parent), - m_alias(alias), - m_founder(founder), - m_version(version), - m_maxUsers(254), - m_autoReset(0), - m_flags() +InMemoryHistory::InMemoryHistory( + const QString &id, const QString &alias, + const protocol::ProtocolVersion &version, const QString &founder, + QObject *parent) + : SessionHistory(id, parent) + , m_alias(alias) + , m_founder(founder) + , m_version(version) + , m_maxUsers(254) + , m_autoReset(0) + , m_flags() { } -std::tuple InMemoryHistory::getBatch(int after) const +std::tuple InMemoryHistory::getBatch(int after) const { if(after >= lastIndex()) - return std::make_tuple(protocol::MessageList(), lastIndex()); + return std::make_tuple(net::MessageList(), lastIndex()); const int offset = qMax(0, after - firstIndex() + 1); - Q_ASSERT(offset namespace server { @@ -16,18 +13,34 @@ namespace server { class InMemoryHistory final : public SessionHistory { Q_OBJECT public: - InMemoryHistory(const QString &id, const QString &alias, const protocol::ProtocolVersion &version, const QString &founder, QObject *parent=nullptr); + InMemoryHistory( + const QString &id, const QString &alias, + const protocol::ProtocolVersion &version, const QString &founder, + QObject *parent = nullptr); - std::tuple getBatch(int after) const override; + std::tuple getBatch(int after) const override; - void terminate() override { /* nothing to do */ } - void cleanupBatches(int) override { /* no caching, nothing to do */ } + void terminate() override + { + // nothing to do + } + + void cleanupBatches(int) override + { + // no caching, nothing to do + } QString idAlias() const override { return m_alias; } QString founderName() const override { return m_founder; } - protocol::ProtocolVersion protocolVersion() const override { return m_version; } + protocol::ProtocolVersion protocolVersion() const override + { + return m_version; + } QByteArray passwordHash() const override { return m_password; } - void setPasswordHash(const QByteArray &password) override { m_password = password; } + void setPasswordHash(const QByteArray &password) override + { + m_password = password; + } QByteArray opwordHash() const override { return m_opword; } void setOpwordHash(const QByteArray &opword) override { m_opword = opword; } int maxUsers() const override { return m_maxUsers; } @@ -36,7 +49,8 @@ class InMemoryHistory final : public SessionHistory { void setTitle(const QString &title) override { m_title = title; } Flags flags() const override { return m_flags; } void setFlags(Flags f) override { m_flags = f; } - void setAutoResetThreshold(uint limit) override { + void setAutoResetThreshold(uint limit) override + { if(sizeLimit() == 0) m_autoReset = limit; else @@ -44,32 +58,61 @@ class InMemoryHistory final : public SessionHistory { } uint autoResetThreshold() const override { return m_autoReset; } - void addAnnouncement(const QString &url) override { m_announcements.insert(url); } - void removeAnnouncement(const QString &url) override { m_announcements.remove(url); } - QStringList announcements() const override { return m_announcements.values(); } + void addAnnouncement(const QString &url) override + { + m_announcements.insert(url); + } + void removeAnnouncement(const QString &url) override + { + m_announcements.remove(url); + } + QStringList announcements() const override + { + return m_announcements.values(); + } - void setAuthenticatedOperator(const QString &authId, bool op) override { + void setAuthenticatedOperator(const QString &authId, bool op) override + { if(authId.isEmpty()) return; - if(op) m_ops.insert(authId); else m_ops.remove(authId); + if(op) + m_ops.insert(authId); + else + m_ops.remove(authId); } - void setAuthenticatedTrust(const QString &authId, bool trusted) override { + void setAuthenticatedTrust(const QString &authId, bool trusted) override + { if(authId.isEmpty()) return; - if(trusted) m_trusted.insert(authId); else m_trusted.remove(authId); + if(trusted) + m_trusted.insert(authId); + else + m_trusted.remove(authId); + } + bool isOperator(const QString &authId) const override + { + return m_ops.contains(authId); + } + bool isTrusted(const QString &authId) const override + { + return m_trusted.contains(authId); } - bool isOperator(const QString &authId) const override { return m_ops.contains(authId); } - bool isTrusted(const QString &authId) const override { return m_trusted.contains(authId); } bool isAuthenticatedOperators() const override { return !m_ops.isEmpty(); } protected: - void historyAdd(const protocol::MessagePtr &msg) override; - void historyReset(const protocol::MessageList &newHistory) override; - void historyAddBan(int, const QString &, const QHostAddress &, const QString &, const QString &) override { /* not persistent */ } - void historyRemoveBan(int) override { /* not persistent */ } + void historyAdd(const net::Message &msg) override; + void historyReset(const net::MessageList &newHistory) override; + void historyAddBan( + int, const QString &, const QHostAddress &, const QString &, + const QString &) override + { /* not persistent */ + } + void historyRemoveBan(int) override + { /* not persistent */ + } private: - protocol::MessageList m_history; + net::MessageList m_history; QSet m_ops; QSet m_trusted; QSet m_announcements; @@ -87,4 +130,3 @@ class InMemoryHistory final : public SessionHistory { } #endif - diff --git a/src/libserver/loginhandler.cpp b/src/libserver/loginhandler.cpp index 22e9be546d..37a4be3ea0 100644 --- a/src/libserver/loginhandler.cpp +++ b/src/libserver/loginhandler.cpp @@ -1,44 +1,39 @@ // SPDX-License-Identifier: GPL-3.0-or-later - #include "libserver/loginhandler.h" +#include "cmake-config/config.h" #include "libserver/client.h" -#include "libserver/session.h" -#include "libserver/sessions.h" #include "libserver/serverconfig.h" #include "libserver/serverlog.h" - -#include "libshared/net/control.h" +#include "libserver/session.h" +#include "libserver/sessions.h" +#include "libshared/net/servercmd.h" #include "libshared/util/authtoken.h" #include "libshared/util/networkaccess.h" #include "libshared/util/validators.h" -#include "cmake-config/config.h" - -#include -#include -#include #include +#include +#include +#include namespace server { -Sessions::~Sessions() -{ -} +Sessions::~Sessions() {} -LoginHandler::LoginHandler(Client *client, Sessions *sessions, ServerConfig *config) - : QObject(client), m_client(client), m_sessions(sessions), m_config(config) +LoginHandler::LoginHandler( + Client *client, Sessions *sessions, ServerConfig *config) + : QObject(client) + , m_client(client) + , m_sessions(sessions) + , m_config(config) { - connect(client, &Client::loginMessage, this, &LoginHandler::handleLoginMessage); + connect( + client, &Client::loginMessage, this, &LoginHandler::handleLoginMessage); } void LoginHandler::startLoginProcess() { m_state = State::WaitForIdent; - protocol::ServerReply greeting; - greeting.type = protocol::ServerReply::LOGIN; - greeting.message = QStringLiteral("Drawpile server %1").arg(cmake_config::version()); - greeting.reply["version"] = cmake_config::proto::server(); - QJsonArray flags; if(m_config->getConfigInt(config::SessionCountLimit) > 1) @@ -46,53 +41,45 @@ void LoginHandler::startLoginProcess() if(m_config->getConfigBool(config::EnablePersistence)) flags << "PERSIST"; if(m_client->hasSslSupport()) { - flags << "TLS" << "SECURE"; + flags << "TLS" + << "SECURE"; m_state = State::WaitForSecure; } if(!m_config->getConfigBool(config::AllowGuests)) flags << "NOGUEST"; - if(m_config->internalConfig().reportUrl.isValid() && m_config->getConfigBool(config::AbuseReport)) + if(m_config->internalConfig().reportUrl.isValid() && + m_config->getConfigBool(config::AbuseReport)) flags << "REPORT"; if(m_config->getConfigBool(config::AllowCustomAvatars)) flags << "AVATAR"; - greeting.reply["flags"] = flags; - // Start by telling who we are - send(greeting); + send(net::ServerReply::makeLoginGreeting( + QStringLiteral("Drawpile server %1").arg(cmake_config::version()), + cmake_config::proto::server(), flags)); - // Client should disconnect upon receiving the above if the version number does not match + // Client should disconnect upon receiving the above if the version number + // does not match } void LoginHandler::announceServerInfo() { const QJsonArray sessions = m_sessions->sessionDescriptions(); - protocol::ServerReply greeting; - greeting.type = protocol::ServerReply::LOGIN; - greeting.message = "Welcome"; - greeting.reply["title"] = m_config->getConfigString(config::ServerTitle); - greeting.reply["sessions"] = sessions; - - if(!send(greeting)) { + QString message = QStringLiteral("Welcome"); + QString title = m_config->getConfigString(config::ServerTitle); + bool wholeMessageSent = send( + title.isEmpty() + ? net::ServerReply::makeLoginSessions(message, sessions) + : net::ServerReply::makeLoginWelcome(message, title, sessions)); + if(!wholeMessageSent) { // Reply was too long to fit in the message envelope! // Split the reply into separate announcements and send it in pieces - protocol::ServerReply piece; - piece.type = greeting.type; - piece.message = greeting.message; - piece.reply["title"] = greeting.reply["title"]; - - if(!greeting.reply["title"].toString().isEmpty()) { - send(piece); + if(!title.isEmpty()) { + send(net::ServerReply::makeLoginTitle(message, title)); } - for(const QJsonValue &session : sessions) { - QJsonArray a; - a << session; - piece.reply["sessions"] = a; - send(piece); - // Include the title as part of the first message, but not the later ones - piece.reply.remove("title"); + send(net::ServerReply::makeLoginSessions(message, {session})); } } } @@ -100,52 +87,40 @@ void LoginHandler::announceServerInfo() void LoginHandler::announceSession(const QJsonObject &session) { Q_ASSERT(session.contains("id")); - - if(m_state != State::WaitForLogin) - return; - - protocol::ServerReply greeting; - greeting.type = protocol::ServerReply::LOGIN; - greeting.message = "New session"; - - QJsonArray s; - s << session; - greeting.reply["sessions"] = s; - - send(greeting); + if(m_state == State::WaitForLogin) { + send(net::ServerReply::makeLoginSessions( + QStringLiteral("New session"), {session})); + } } void LoginHandler::announceSessionEnd(const QString &id) { - if(m_state != State::WaitForLogin) - return; - - protocol::ServerReply greeting; - greeting.type = protocol::ServerReply::LOGIN; - greeting.message = "Session ended"; - - QJsonArray s; - s << id; - greeting.reply["remove"] = s; - - send(greeting); + if(m_state == State::WaitForLogin) { + send(net::ServerReply::makeLoginRemoveSessions( + QStringLiteral("Session ended"), {id})); + } } -void LoginHandler::handleLoginMessage(protocol::MessagePtr msg) +void LoginHandler::handleLoginMessage(const net::Message &msg) { - if(msg->type() != protocol::MSG_COMMAND) { - m_client->log(Log().about(Log::Level::Error, Log::Topic::RuleBreak).message("Login handler was passed a non-login message")); + if(msg.type() != DP_MSG_SERVER_COMMAND) { + m_client->log( + Log() + .about(Log::Level::Error, Log::Topic::RuleBreak) + .message("Login handler was passed a non-login message")); return; } - protocol::ServerCommand cmd = msg.cast().cmd(); + net::ServerCommand cmd = net::ServerCommand::fromMessage(msg); if(m_state == State::WaitForSecure) { // Secure mode: wait for STARTTLS before doing anything if(cmd.cmd == "startTls") { handleStarttls(); } else { - m_client->log(Log().about(Log::Level::Error, Log::Topic::RuleBreak).message("Client did not upgrade to TLS mode!")); + m_client->log(Log() + .about(Log::Level::Error, Log::Topic::RuleBreak) + .message("Client did not upgrade to TLS mode!")); sendError("tlsRequired", "TLS required"); } @@ -154,8 +129,14 @@ void LoginHandler::handleLoginMessage(protocol::MessagePtr msg) if(cmd.cmd == "ident") { handleIdentMessage(cmd); } else { - m_client->log(Log().about(Log::Level::Error, Log::Topic::RuleBreak).message("Invalid login command (while waiting for ident): " + cmd.cmd)); - m_client->disconnectClient(Client::DisconnectionReason::Error, "invalid message"); + m_client->log( + Log() + .about(Log::Level::Error, Log::Topic::RuleBreak) + .message( + "Invalid login command (while waiting for ident): " + + cmd.cmd)); + m_client->disconnectClient( + Client::DisconnectionReason::Error, "invalid message"); } } else { if(cmd.cmd == "host") { @@ -165,8 +146,14 @@ void LoginHandler::handleLoginMessage(protocol::MessagePtr msg) } else if(cmd.cmd == "report") { handleAbuseReport(cmd); } else { - m_client->log(Log().about(Log::Level::Error, Log::Topic::RuleBreak).message("Invalid login command (while waiting for join/host): " + cmd.cmd)); - m_client->disconnectClient(Client::DisconnectionReason::Error, "invalid message"); + m_client->log(Log() + .about(Log::Level::Error, Log::Topic::RuleBreak) + .message( + "Invalid login command (while waiting for " + "join/host): " + + cmd.cmd)); + m_client->disconnectClient( + Client::DisconnectionReason::Error, "invalid message"); } } } @@ -184,41 +171,50 @@ static QStringList jsonArrayToStringList(const QJsonArray &a) } #endif -void LoginHandler::handleIdentMessage(const protocol::ServerCommand &cmd) +void LoginHandler::handleIdentMessage(const net::ServerCommand &cmd) { - if(cmd.args.size()!=1 && cmd.args.size()!=2) { + if(cmd.args.size() != 1 && cmd.args.size() != 2) { sendError("syntax", "Expected username and (optional) password"); return; } const QString username = cmd.args[0].toString(); - const QString password = cmd.args.size()>1 ? cmd.args[1].toString() : QString(); + const QString password = + cmd.args.size() > 1 ? cmd.args[1].toString() : QString(); if(!validateUsername(username)) { sendError("badUsername", "Invalid username"); return; } - const RegisteredUser userAccount = m_config->getUserAccount(username, password); + const RegisteredUser userAccount = + m_config->getUserAccount(username, password); - if(userAccount.status != RegisteredUser::NotFound && cmd.kwargs.contains("extauth")) { - // This should never happen. If it does, it means there's a bug in the client - // or someone is probing for bugs in the server. - sendError("extAuthError", "Cannot use extauth with an internal user account!"); + if(userAccount.status != RegisteredUser::NotFound && + cmd.kwargs.contains("extauth")) { + // This should never happen. If it does, it means there's a bug in the + // client or someone is probing for bugs in the server. + sendError( + "extAuthError", + "Cannot use extauth with an internal user account!"); return; } - if(cmd.kwargs.contains("avatar") && m_config->getConfigBool(config::AllowCustomAvatars)) { + if(cmd.kwargs.contains("avatar") && + m_config->getConfigBool(config::AllowCustomAvatars)) { // TODO validate - m_client->setAvatar(QByteArray::fromBase64(cmd.kwargs["avatar"].toString().toUtf8())); + m_client->setAvatar( + QByteArray::fromBase64(cmd.kwargs["avatar"].toString().toUtf8())); } switch(userAccount.status) { case RegisteredUser::NotFound: { - // Account not found in internal user list. Allow guest login (if enabled) - // or require external authentication + // Account not found in internal user list. Allow guest login (if + // enabled) or require external authentication const bool allowGuests = m_config->getConfigBool(config::AllowGuests); - const bool useExtAuth = m_config->internalConfig().extAuthUrl.isValid() && m_config->getConfigBool(config::UseExtAuth); + const bool useExtAuth = + m_config->internalConfig().extAuthUrl.isValid() && + m_config->getConfigBool(config::UseExtAuth); if(useExtAuth) { #ifdef HAVE_LIBSODIUM @@ -228,13 +224,18 @@ void LoginHandler::handleIdentMessage(const protocol::ServerCommand &cmd) sendError("extAuthError", "Ext auth not requested!"); return; } - const AuthToken extAuthToken(cmd.kwargs["extauth"].toString().toUtf8()); - const QByteArray key = QByteArray::fromBase64(m_config->getConfigString(config::ExtAuthKey).toUtf8()); + const AuthToken extAuthToken( + cmd.kwargs["extauth"].toString().toUtf8()); + const QByteArray key = QByteArray::fromBase64( + m_config->getConfigString(config::ExtAuthKey).toUtf8()); if(!extAuthToken.checkSignature(key)) { - sendError("extAuthError", "Ext auth token signature mismatch!"); + sendError( + "extAuthError", "Ext auth token signature mismatch!"); return; } - if(!extAuthToken.validatePayload(m_config->getConfigString(config::ExtAuthGroup), m_extauth_nonce)) { + if(!extAuthToken.validatePayload( + m_config->getConfigString(config::ExtAuthGroup), + m_extauth_nonce)) { sendError("extAuthError", "Ext auth token is invalid!"); return; } @@ -243,34 +244,35 @@ void LoginHandler::handleIdentMessage(const protocol::ServerCommand &cmd) const QJsonObject ea = extAuthToken.payload(); const QJsonValue uid = ea["uid"]; - // We need some unique identifier. If the server didn't provide one, - // the username is better than nothing. - QString extAuthId = uid.isDouble() ? QString::number(uid.toInt()) : uid.toString(); + // We need some unique identifier. If the server didn't provide + // one, the username is better than nothing. + QString extAuthId = uid.isDouble() + ? QString::number(uid.toInt()) + : uid.toString(); if(extAuthId.isEmpty()) extAuthId = ea["username"].toString(); // Prefix to identify this auth ID as an ext-auth ID - extAuthId = m_config->internalConfig().extAuthUrl.host() + ":" + extAuthId; + extAuthId = m_config->internalConfig().extAuthUrl.host() + ":" + + extAuthId; QByteArray avatar; if(m_config->getConfigBool(config::ExtAuthAvatars)) avatar = extAuthToken.avatar(); authLoginOk( - ea["username"].toString(), - extAuthId, - jsonArrayToStringList(ea["flags"].toArray()), - avatar, + ea["username"].toString(), extAuthId, + jsonArrayToStringList(ea["flags"].toArray()), avatar, m_config->getConfigBool(config::ExtAuthMod), - m_config->getConfigBool(config::ExtAuthHost) - ); + m_config->getConfigBool(config::ExtAuthHost)); } else { // No ext-auth token provided: request it now - // If both guest logins and ExtAuth is enabled, we must query the auth server first - // to determine if guest login is possible for this user. - // If guest logins are not enabled, we always just request ext-auth + // If both guest logins and ExtAuth is enabled, we must query + // the auth server first to determine if guest login is possible + // for this user. If guest logins are not enabled, we always + // just request ext-auth if(allowGuests) extAuthGuestLogin(username); else @@ -279,7 +281,9 @@ void LoginHandler::handleIdentMessage(const protocol::ServerCommand &cmd) #else // This should never be reached - sendError("extAuthError", "Server misconfiguration: ext-auth support not compiled in."); + sendError( + "extAuthError", + "Server misconfiguration: ext-auth support not compiled in."); #endif return; @@ -291,17 +295,15 @@ void LoginHandler::handleIdentMessage(const protocol::ServerCommand &cmd) } } Q_FALLTHROUGH(); // fall through to badpass if guest logins are disabled - } + } case RegisteredUser::BadPass: if(password.isEmpty()) { - // No password: tell client that guest login is not possible (for this username) + // No password: tell client that guest login is not possible (for + // this username) m_state = State::WaitForIdent; - - protocol::ServerReply identReply; - identReply.type = protocol::ServerReply::RESULT; - identReply.message = "Password needed"; - identReply.reply["state"] = "needPassword"; - send(identReply); + send(net::ServerReply::makeResultPasswordNeeded( + QStringLiteral("Password needed"), + QStringLiteral("needPassword"))); } else { sendError("badPassword", "Incorrect password"); @@ -315,18 +317,15 @@ void LoginHandler::handleIdentMessage(const protocol::ServerCommand &cmd) case RegisteredUser::Ok: // Yay, username and password were valid! authLoginOk( - username, - QStringLiteral("internal:%1").arg(userAccount.userId), - userAccount.flags, - QByteArray(), - true, - true - ); + username, QStringLiteral("internal:%1").arg(userAccount.userId), + userAccount.flags, QByteArray(), true, true); break; } } -void LoginHandler::authLoginOk(const QString &username, const QString &authId, const QStringList &flags, const QByteArray &avatar, bool allowMod, bool allowHost) +void LoginHandler::authLoginOk( + const QString &username, const QString &authId, const QStringList &flags, + const QByteArray &avatar, bool allowMod, bool allowHost) { Q_ASSERT(!authId.isEmpty()); @@ -334,33 +333,27 @@ void LoginHandler::authLoginOk(const QString &username, const QString &authId, c m_client->setAuthId(authId); m_client->setAuthFlags(flags); - protocol::ServerReply identReply; - identReply.type = protocol::ServerReply::RESULT; - identReply.message = "Authenticated login OK!"; - identReply.reply["state"] = "identOk"; - identReply.reply["flags"] = QJsonArray::fromStringList(flags); - identReply.reply["ident"] = m_client->username(); - identReply.reply["guest"] = false; - m_client->setModerator(flags.contains("MOD") && allowMod); if(!avatar.isEmpty()) m_client->setAvatar(avatar); m_hostPrivilege = flags.contains("HOST") && allowHost; m_state = State::WaitForLogin; - send(identReply); + send(net::ServerReply::makeResultLoginOk( + QStringLiteral("Authenticated login OK!"), QStringLiteral("identOk"), + QJsonArray::fromStringList(flags), m_client->username(), false)); announceServerInfo(); - } /** - * @brief Make a request to the ext-auth server and check if guest login is possible for the given username + * @brief Make a request to the ext-auth server and check if guest login is + * possible for the given username * * If guest login is possible, do that. * Otherwise request external authentication. * - * If the authserver cannot be reached, guest login is permitted (fallback mode) or - * not. + * If the authserver cannot be reached, guest login is permitted (fallback mode) + * or not. * @param username */ void LoginHandler::extAuthGuestLogin(const QString &username) @@ -374,20 +367,28 @@ void LoginHandler::extAuthGuestLogin(const QString &username) if(!authGroup.isEmpty()) o["group"] = authGroup; - m_client->log(Log().about(Log::Level::Info, Log::Topic::Status).message(QStringLiteral("Querying auth server for %1...").arg(username))); - QNetworkReply *reply = networkaccess::getInstance()->post(req, QJsonDocument(o).toJson()); + m_client->log(Log() + .about(Log::Level::Info, Log::Topic::Status) + .message(QStringLiteral("Querying auth server for %1...") + .arg(username))); + QNetworkReply *reply = + networkaccess::getInstance()->post(req, QJsonDocument(o).toJson()); connect(reply, &QNetworkReply::finished, this, [reply, username, this]() { reply->deleteLater(); if(m_state != State::WaitForIdent) { - sendError("extauth", "Received auth serveer reply in unexpected state"); + sendError( + "extauth", "Received auth serveer reply in unexpected state"); return; } bool fail = false; if(reply->error() != QNetworkReply::NoError) { fail = true; - m_client->log(Log().about(Log::Level::Warn, Log::Topic::Status).message("Auth server error: " + reply->errorString())); + m_client->log( + Log() + .about(Log::Level::Warn, Log::Topic::Status) + .message("Auth server error: " + reply->errorString())); } QJsonDocument doc; @@ -396,7 +397,11 @@ void LoginHandler::extAuthGuestLogin(const QString &username) doc = QJsonDocument::fromJson(reply->readAll(), &error); if(error.error != QJsonParseError::NoError) { fail = true; - m_client->log(Log().about(Log::Level::Warn, Log::Topic::Status).message("Auth server JSON parse error: " + error.errorString())); + m_client->log(Log() + .about(Log::Level::Warn, Log::Topic::Status) + .message( + "Auth server JSON parse error: " + + error.errorString())); } } @@ -409,7 +414,6 @@ void LoginHandler::extAuthGuestLogin(const QString &username) sendError("noExtAuth", "Authentication server is unavailable!"); } return; - } const QJsonObject obj = doc.object(); @@ -421,7 +425,9 @@ void LoginHandler::extAuthGuestLogin(const QString &username) guestLogin(username); } else if(status == "outgroup") { - sendError("extauthOutgroup", "This username cannot log in to this server"); + sendError( + "extauthOutgroup", + "This username cannot log in to this server"); } else { sendError("extauth", "Unexpected ext-auth response: " + status); @@ -434,19 +440,17 @@ void LoginHandler::requestExtAuth() #ifdef HAVE_LIBSODIUM Q_ASSERT(m_extauth_nonce == 0); m_extauth_nonce = AuthToken::generateNonce(); - - protocol::ServerReply identReply; - identReply.type = protocol::ServerReply::RESULT; - identReply.message = "External authentication needed"; - identReply.reply["state"] = "needExtAuth"; - identReply.reply["extauthurl"] = m_config->internalConfig().extAuthUrl.toString(); - identReply.reply["nonce"] = QString::number(m_extauth_nonce, 16); - identReply.reply["group"] = m_config->getConfigString(config::ExtAuthGroup); - identReply.reply["avatar"] = m_client->avatar().isEmpty() && m_config->getConfigBool(config::ExtAuthAvatars); - - send(identReply); + send(net::ServerReply::makeResultExtAuthNeeded( + QStringLiteral("External authentication needed"), + QStringLiteral("needExtAuth"), + m_config->internalConfig().extAuthUrl.toString(), + QString::number(m_extauth_nonce, 16), + m_config->getConfigString(config::ExtAuthGroup), + m_client->avatar().isEmpty() && + m_config->getConfigBool(config::ExtAuthAvatars))); #else - qFatal("Bug: requestExtAuth() called, even though libsodium is not compiled in!"); + qFatal("Bug: requestExtAuth() called, even though libsodium is not " + "compiled in!"); #endif } @@ -459,15 +463,9 @@ void LoginHandler::guestLogin(const QString &username) m_client->setUsername(username); m_state = State::WaitForLogin; - - protocol::ServerReply identReply; - identReply.type = protocol::ServerReply::RESULT; - identReply.message = "Guest login OK!"; - identReply.reply["state"] = "identOk"; - identReply.reply["flags"] = QJsonArray(); - identReply.reply["ident"] = m_client->username(); - identReply.reply["guest"] = true; - send(identReply); + send(net::ServerReply::makeResultLoginOk( + QStringLiteral("Guest login OK!"), QStringLiteral("identOk"), {}, + m_client->username(), true)); announceServerInfo(); } @@ -475,7 +473,8 @@ void LoginHandler::guestLogin(const QString &username) static QJsonArray sessionFlags(const Session *session) { QJsonArray flags; - // Note: this is "NOAUTORESET" for backward compatibility. In 3.0, we should change it to "AUTORESET" + // Note: this is "NOAUTORESET" for backward compatibility. In 3.0, we should + // change it to "AUTORESET" if(!session->supportsAutoReset()) flags << "NOAUTORESET"; // TODO for version 3.0: PERSIST should be a session specific flag @@ -483,7 +482,7 @@ static QJsonArray sessionFlags(const Session *session) return flags; } -void LoginHandler::handleHostMessage(const protocol::ServerCommand &cmd) +void LoginHandler::handleHostMessage(const net::ServerCommand &cmd) { Q_ASSERT(!m_client->username().isEmpty()); @@ -493,7 +492,9 @@ void LoginHandler::handleHostMessage(const protocol::ServerCommand &cmd) return; } - protocol::ProtocolVersion protocolVersion = protocol::ProtocolVersion::fromString(cmd.kwargs.value("protocol").toString()); + protocol::ProtocolVersion protocolVersion = + protocol::ProtocolVersion::fromString( + cmd.kwargs.value("protocol").toString()); if(!protocolVersion.isValid()) { sendError("syntax", "Unparseable protocol version"); @@ -502,7 +503,7 @@ void LoginHandler::handleHostMessage(const protocol::ServerCommand &cmd) int userId = cmd.kwargs.value("user_id").toInt(); - if(userId < 1 || userId>254) { + if(userId < 1 || userId > 254) { sendError("syntax", "Invalid user ID (must be in range 1-254)"); return; } @@ -521,11 +522,8 @@ void LoginHandler::handleHostMessage(const protocol::ServerCommand &cmd) Session *session; QString sessionErrorCode; std::tie(session, sessionErrorCode) = m_sessions->createSession( - Ulid::make().toString(), - sessionAlias, - protocolVersion, - m_client->username() - ); + Ulid::make().toString(), sessionAlias, protocolVersion, + m_client->username()); if(!session) { QString msg; @@ -545,18 +543,14 @@ void LoginHandler::handleHostMessage(const protocol::ServerCommand &cmd) if(cmd.kwargs["password"].isString()) session->history()->setPassword(cmd.kwargs["password"].toString()); - // Mark login phase as complete. No more login messages will be sent to this user - protocol::ServerReply reply; - reply.type = protocol::ServerReply::RESULT; - reply.message = "Starting new session!"; - reply.reply["state"] = "host"; - - QJsonObject joinInfo; - joinInfo["id"] = sessionAlias.isEmpty() ? session->id() : sessionAlias; - joinInfo["user"] = userId; - joinInfo["flags"] = sessionFlags(session); - reply.reply["join"] = joinInfo; - send(reply); + // Mark login phase as complete. + // No more login messages will be sent to this user. + send(net::ServerReply::makeResultJoinHost( + QStringLiteral("Starting new session!"), QStringLiteral("host"), + {{QStringLiteral("id"), + sessionAlias.isEmpty() ? session->id() : sessionAlias}, + {QStringLiteral("user"), userId}, + {QStringLiteral("flags"), sessionFlags(session)}})); logClientInfo(cmd); m_complete = true; @@ -565,10 +559,10 @@ void LoginHandler::handleHostMessage(const protocol::ServerCommand &cmd) deleteLater(); } -void LoginHandler::handleJoinMessage(const protocol::ServerCommand &cmd) +void LoginHandler::handleJoinMessage(const net::ServerCommand &cmd) { Q_ASSERT(!m_client->username().isEmpty()); - if(cmd.args.size()!=1) { + if(cmd.args.size() != 1) { sendError("syntax", "Expected session ID"); return; } @@ -583,7 +577,8 @@ void LoginHandler::handleJoinMessage(const protocol::ServerCommand &cmd) if(!m_client->isModerator()) { // Non-moderators have to obey access restrictions - if(session->history()->banlist().isBanned(m_client->peerAddress(), m_client->authId())) { + if(session->history()->banlist().isBanned( + m_client->peerAddress(), m_client->authId())) { sendError("banned", "You have been banned from this session"); return; } @@ -591,12 +586,14 @@ void LoginHandler::handleJoinMessage(const protocol::ServerCommand &cmd) sendError("closed", "This session is closed"); return; } - if(session->history()->hasFlag(SessionHistory::AuthOnly) && !m_client->isAuthenticated()) { + if(session->history()->hasFlag(SessionHistory::AuthOnly) && + !m_client->isAuthenticated()) { sendError("authOnly", "This session does not allow guest logins"); return; } - if(!session->history()->checkPassword(cmd.kwargs.value("password").toString())) { + if(!session->history()->checkPassword( + cmd.kwargs.value("password").toString())) { sendError("badPassword", "Incorrect password"); return; } @@ -607,26 +604,25 @@ void LoginHandler::handleJoinMessage(const protocol::ServerCommand &cmd) sendError("nameInuse", "This username is already in use"); return; #else - // Allow identical usernames in debug builds, so I don't have to keep changing - // the username when testing. There is no technical requirement for unique usernames; - // the limitation is solely for the benefit of the human users. - m_client->log(Log().about(Log::Level::Warn, Log::Topic::RuleBreak).message("Username clash ignored because this is a debug build.")); + // Allow identical usernames in debug builds, so I don't have to keep + // changing the username when testing. There is no technical requirement + // for unique usernames; the limitation is solely for the benefit of the + // human users. + m_client->log( + Log() + .about(Log::Level::Warn, Log::Topic::RuleBreak) + .message( + "Username clash ignored because this is a debug build.")); #endif } // Ok, join the session session->assignId(m_client); - - protocol::ServerReply reply; - reply.type = protocol::ServerReply::RESULT; - reply.message = "Joining a session!"; - reply.reply["state"] = "join"; - QJsonObject joinInfo; - joinInfo["id"] = session->aliasOrId(); - joinInfo["user"] = m_client->id(); - joinInfo["flags"] = sessionFlags(session); - reply.reply["join"] = joinInfo; - send(reply); + send(net::ServerReply::makeResultJoinHost( + QStringLiteral("Joining a session!"), QStringLiteral("join"), + {{QStringLiteral("id"), session->aliasOrId()}, + {QStringLiteral("user"), m_client->id()}, + {QStringLiteral("flags"), sessionFlags(session)}})); logClientInfo(cmd); m_complete = true; @@ -635,14 +631,12 @@ void LoginHandler::handleJoinMessage(const protocol::ServerCommand &cmd) deleteLater(); } -void LoginHandler::logClientInfo(const protocol::ServerCommand &cmd) +void LoginHandler::logClientInfo(const net::ServerCommand &cmd) { QJsonObject info; QString keys[] = { - QStringLiteral("app_version"), - QStringLiteral("protocol_version"), - QStringLiteral("qt_version"), - QStringLiteral("os"), + QStringLiteral("app_version"), QStringLiteral("protocol_version"), + QStringLiteral("qt_version"), QStringLiteral("os"), QStringLiteral("s"), }; for(const QString &key : keys) { @@ -662,14 +656,17 @@ void LoginHandler::logClientInfo(const protocol::ServerCommand &cmd) if(m_client->isAuthenticated()) { info["auth_id"] = m_client->authId(); } - m_client->log(Log().about(Log::Level::Info, Log::Topic::ClientInfo) - .message(QJsonDocument(info).toJson(QJsonDocument::Compact))); + m_client->log( + Log() + .about(Log::Level::Info, Log::Topic::ClientInfo) + .message(QJsonDocument(info).toJson(QJsonDocument::Compact))); } } -void LoginHandler::handleAbuseReport(const protocol::ServerCommand &cmd) +void LoginHandler::handleAbuseReport(const net::ServerCommand &cmd) { - Session *s = m_sessions->getSessionById(cmd.kwargs["session"].toString(), false); + Session *s = + m_sessions->getSessionById(cmd.kwargs["session"].toString(), false); if(s) { s->sendAbuseReport(m_client, 0, cmd.kwargs["reason"].toString()); } @@ -678,32 +675,31 @@ void LoginHandler::handleAbuseReport(const protocol::ServerCommand &cmd) void LoginHandler::handleStarttls() { if(!m_client->hasSslSupport()) { - // Note. Well behaved clients shouldn't send STARTTLS if TLS was not listed in server features. + // Note. Well behaved clients shouldn't send STARTTLS if TLS was not + // listed in server features. sendError("noTls", "TLS not supported"); return; } if(m_client->isSecure()) { - sendError("alreadySecure", "Connection already secured"); // shouldn't happen normally + sendError( + "alreadySecure", + "Connection already secured"); // shouldn't happen normally return; } - protocol::ServerReply reply; - reply.type = protocol::ServerReply::LOGIN; - reply.message = "Start TLS now!"; - reply.reply["startTls"] = true; - send(reply); + send(net::ServerReply::makeResultStartTls( + QStringLiteral("Start TLS now!"), true)); m_client->startTls(); m_state = State::WaitForIdent; } -bool LoginHandler::send(const protocol::ServerReply &cmd) +bool LoginHandler::send(const net::Message &msg) { if(!m_complete) { - protocol::MessagePtr msg(new protocol::Command(0, cmd)); - if(msg.cast().isOversize()) { - qWarning("Oversize login message %s", qPrintable(cmd.message)); + if(msg.isNull()) { + qWarning("Login message is null (input oversized?)"); return false; } m_client->sendDirectMessage(msg); @@ -713,12 +709,9 @@ bool LoginHandler::send(const protocol::ServerReply &cmd) void LoginHandler::sendError(const QString &code, const QString &message) { - protocol::ServerReply r; - r.type = protocol::ServerReply::ERROR; - r.message = message; - r.reply["code"] = code; - send(r); - m_client->disconnectClient(Client::DisconnectionReason::Error, "Login error"); + send(net::ServerReply::makeError(message, code)); + m_client->disconnectClient( + Client::DisconnectionReason::Error, "Login error"); } } diff --git a/src/libserver/loginhandler.h b/src/libserver/loginhandler.h index dc12382d6b..649936a909 100644 --- a/src/libserver/loginhandler.h +++ b/src/libserver/loginhandler.h @@ -1,17 +1,14 @@ // SPDX-License-Identifier: GPL-3.0-or-later - #ifndef DP_SERVER_LOGINHANDLER_H #define DP_SERVER_LOGINHANDLER_H - #include "libshared/net/message.h" - +#include #include #include -#include -namespace protocol { - struct ServerCommand; - struct ServerReply; +namespace net { +struct ServerCommand; +struct ServerReply; } namespace server { @@ -39,13 +36,15 @@ class ServerConfig; * * S: SESSION LIST UPDATES * - * - Note. Server may send updates to session list and title until the client has made a choice - + * - Note. Server may send updates to session list and title until the client + * has made a choice - * * C: HOST or JOIN session * * S: OK or ERROR * - * - if OK, the client is added to the session. If the client is hosting, initial state must be uploaded next. - + * - if OK, the client is added to the session. If the client is hosting, + * initial state must be uploaded next. - * * Notes: * ------ @@ -63,8 +62,7 @@ class ServerConfig; * If the ID was specified by the user (vanity ID), it is prefixed with '!' * */ -class LoginHandler final : public QObject -{ +class LoginHandler final : public QObject { Q_OBJECT public: LoginHandler(Client *client, Sessions *sessions, ServerConfig *config); @@ -76,26 +74,25 @@ public slots: void announceSessionEnd(const QString &id); private slots: - void handleLoginMessage(protocol::MessagePtr message); + void handleLoginMessage(const net::Message &msg); private: - enum class State { - WaitForSecure, - WaitForIdent, - WaitForLogin - }; + enum class State { WaitForSecure, WaitForIdent, WaitForLogin }; void announceServerInfo(); - void handleIdentMessage(const protocol::ServerCommand &cmd); - void handleHostMessage(const protocol::ServerCommand &cmd); - void handleJoinMessage(const protocol::ServerCommand &cmd); - void logClientInfo(const protocol::ServerCommand &cmd); - void handleAbuseReport(const protocol::ServerCommand &cmd); + void handleIdentMessage(const net::ServerCommand &cmd); + void handleHostMessage(const net::ServerCommand &cmd); + void handleJoinMessage(const net::ServerCommand &cmd); + void logClientInfo(const net::ServerCommand &cmd); + void handleAbuseReport(const net::ServerCommand &cmd); void handleStarttls(); void requestExtAuth(); void guestLogin(const QString &username); - void authLoginOk(const QString &username, const QString &authId, const QStringList &flags, const QByteArray &avatar, bool allowMod, bool allowHost); - bool send(const protocol::ServerReply &cmd); + void authLoginOk( + const QString &username, const QString &authId, + const QStringList &flags, const QByteArray &avatar, bool allowMod, + bool allowHost); + bool send(const net::Message &msg); void sendError(const QString &code, const QString &message); void extAuthGuestLogin(const QString &username); @@ -112,4 +109,3 @@ private slots: } #endif // LOGINHANDLER_H - diff --git a/src/libserver/opcommands.cpp b/src/libserver/opcommands.cpp index b544707df3..25607209ca 100644 --- a/src/libserver/opcommands.cpp +++ b/src/libserver/opcommands.cpp @@ -1,13 +1,11 @@ // SPDX-License-Identifier: GPL-3.0-or-later - #include "libserver/opcommands.h" #include "libserver/client.h" -#include "libserver/session.h" #include "libserver/serverlog.h" -#include "libshared/net/control.h" -#include "libshared/net/meta.h" +#include "libserver/session.h" +#include "libshared/net/servercmd.h" #include "libshared/util/passwordhash.h" - +#include #include #include #include @@ -16,32 +14,54 @@ namespace server { namespace { -class CmdError { +class CmdResult { public: - CmdError(const QString &msg) : m_msg(msg) {} + static CmdResult ok() { return CmdResult{true, QString{}}; } - const QString &message() const { return m_msg; } + static CmdResult err(const QString &message) + { + return CmdResult{false, message}; + } + + bool success() const { return m_success; } + const QString &message() const { return m_message; } private: - QString m_msg; + CmdResult(bool success, const QString &message) + : m_success(success) + , m_message(message) + { + } + + bool m_success; + QString m_message; }; -typedef void (*SrvCommandFn)(Client *, const QJsonArray &, const QJsonObject &); +typedef CmdResult (*SrvCommandFn)( + Client *, const QJsonArray &, const QJsonObject &); class SrvCommand { public: enum Mode { - NONOP, // usable by all + NONOP, // usable by all DEPUTY, // needs at least deputy privileges - OP, // needs operator privileges - MOD // needs moderator privileges + OP, // needs operator privileges + MOD // needs moderator privileges }; - SrvCommand(const QString &name, SrvCommandFn fn, Mode mode=OP) - : m_fn(fn), m_name(name), m_mode(mode) - {} + SrvCommand(const QString &name, SrvCommandFn fn, Mode mode = OP) + : m_fn(fn) + , m_name(name) + , m_mode(mode) + { + } + + CmdResult + call(Client *c, const QJsonArray &args, const QJsonObject &kwargs) const + { + return m_fn(c, args, kwargs); + } - void call(Client *c, const QJsonArray &args, const QJsonObject &kwargs) const { m_fn(c, args, kwargs); } const QString &name() const { return m_name; } Mode mode() const { return m_mode; } @@ -60,192 +80,243 @@ struct SrvCommandSet { const SrvCommandSet COMMANDS; -void readyToAutoReset(Client *client, const QJsonArray &args, const QJsonObject &kwargs) +CmdResult readyToAutoReset( + Client *client, const QJsonArray &args, const QJsonObject &kwargs) { Q_UNUSED(args); Q_UNUSED(kwargs); client->session()->readyToAutoReset(client->id()); + return CmdResult::ok(); } -void initBegin(Client *client, const QJsonArray &args, const QJsonObject &kwargs) +CmdResult +initBegin(Client *client, const QJsonArray &args, const QJsonObject &kwargs) { Q_UNUSED(args); Q_UNUSED(kwargs); client->session()->handleInitBegin(client->id()); + return CmdResult::ok(); } -void initComplete(Client *client, const QJsonArray &args, const QJsonObject &kwargs) +CmdResult +initComplete(Client *client, const QJsonArray &args, const QJsonObject &kwargs) { Q_UNUSED(args); Q_UNUSED(kwargs); client->session()->handleInitComplete(client->id()); + return CmdResult::ok(); } -void initCancel(Client *client, const QJsonArray &args, const QJsonObject &kwargs) +CmdResult +initCancel(Client *client, const QJsonArray &args, const QJsonObject &kwargs) { Q_UNUSED(args); Q_UNUSED(kwargs); client->session()->handleInitCancel(client->id()); + return CmdResult::ok(); } -void sessionConf(Client *client, const QJsonArray &args, const QJsonObject &kwargs) +CmdResult +sessionConf(Client *client, const QJsonArray &args, const QJsonObject &kwargs) { Q_UNUSED(args); client->session()->setSessionConfig(kwargs, client); + return CmdResult::ok(); } -void opWord(Client *client, const QJsonArray &args, const QJsonObject &kwargs) +CmdResult +opWord(Client *client, const QJsonArray &args, const QJsonObject &kwargs) { Q_UNUSED(kwargs); if(args.size() != 1) - throw CmdError("Expected one argument: opword"); + return CmdResult::err("Expected one argument: opword"); const QByteArray opwordHash = client->session()->history()->opwordHash(); if(opwordHash.isEmpty()) - throw CmdError("No opword set"); + return CmdResult::err("No opword set"); if(passwordhash::check(args.at(0).toString(), opwordHash)) { client->session()->changeOpStatus(client->id(), true, "password"); + return CmdResult::ok(); } else { - throw CmdError("Incorrect password"); + return CmdResult::err("Incorrect password"); } } -Client *_getClient(Session *session, const QJsonValue &idOrName) +static CmdResult +getClient(Session *session, const QJsonValue &idOrName, Client *&outClient) { - Client *c = nullptr; + Client *c; if(idOrName.isDouble()) { // ID number const int id = idOrName.toInt(); - if(id<1 || id > 254) - throw CmdError("invalid user id: " + QString::number(id)); + if(id < 1 || id > 254) + return CmdResult::err("invalid user id: " + QString::number(id)); c = session->getClientById(id); - } else if(idOrName.isString()){ + } else if(idOrName.isString()) { // Username c = session->getClientByUsername(idOrName.toString()); } else { - throw CmdError("invalid user ID or name"); + return CmdResult::err("invalid user ID or name"); } - if(!c) - throw CmdError("user not found"); - return c; + if(c) { + outClient = c; + return CmdResult::ok(); + } else { + return CmdResult::err("user not found"); + } } -void kickUser(Client *client, const QJsonArray &args, const QJsonObject &kwargs) +CmdResult +kickUser(Client *client, const QJsonArray &args, const QJsonObject &kwargs) { - if(args.size()!=1) - throw CmdError("Expected one argument: user ID or name"); + if(args.size() != 1) + return CmdResult::err("Expected one argument: user ID or name"); const bool ban = kwargs["ban"].toBool(); if(ban && client->session()->hasPastClientWithId(args.at(0).toInt())) { // Retroactive ban - const auto target = client->session()->getPastClientById(args.at(0).toInt()); + const auto target = + client->session()->getPastClientById(args.at(0).toInt()); if(target.isBannable) { client->session()->addBan(target, client->username()); - client->session()->messageAll(target.username + " banned by " + client->username(), false); + client->session()->messageAll( + target.username + " banned by " + client->username(), false); + return CmdResult::ok(); } else { - throw CmdError(target.username + " cannot be banned."); + return CmdResult::err(target.username + " cannot be banned."); } - return; } - Client *target = _getClient(client->session(), args.at(0)); + Client *target; + CmdResult result = getClient(client->session(), args.at(0), target); + if(!result.success()) { + return result; + } + if(target == client) - throw CmdError("cannot kick self"); + return CmdResult::err("cannot kick self"); if(target->isModerator()) - throw CmdError("cannot kick moderators"); + return CmdResult::err("cannot kick moderators"); if(client->isDeputy()) { if(target->isOperator() || target->isTrusted()) - throw CmdError("cannot kick trusted users"); + return CmdResult::err("cannot kick trusted users"); } if(ban) { client->session()->addBan(target, client->username()); - client->session()->messageAll(target->username() + " banned by " + client->username(), false); + client->session()->messageAll( + target->username() + " banned by " + client->username(), false); } else { - client->session()->messageAll(target->username() + " kicked by " + client->username(), false); + client->session()->messageAll( + target->username() + " kicked by " + client->username(), false); } - target->disconnectClient(Client::DisconnectionReason::Kick, client->username()); + target->disconnectClient( + Client::DisconnectionReason::Kick, client->username()); + return CmdResult::ok(); } -void removeBan(Client *client, const QJsonArray &args, const QJsonObject &kwargs) +CmdResult +removeBan(Client *client, const QJsonArray &args, const QJsonObject &kwargs) { Q_UNUSED(kwargs); - if(args.size()!=1) - throw CmdError("Expected one argument: ban entry ID"); + if(args.size() != 1) + return CmdResult::err("Expected one argument: ban entry ID"); client->session()->removeBan(args.at(0).toInt(), client->username()); + return CmdResult::ok(); } -void killSession(Client *client, const QJsonArray &args, const QJsonObject &kwargs) +CmdResult +killSession(Client *client, const QJsonArray &args, const QJsonObject &kwargs) { Q_UNUSED(args); Q_UNUSED(kwargs); - client->session()->messageAll(QString("Session shut down by moderator (%1)").arg(client->username()), true); + client->session()->messageAll( + QString("Session shut down by moderator (%1)").arg(client->username()), + true); client->session()->killSession(); + return CmdResult::ok(); } -void announceSession(Client *client, const QJsonArray &args, const QJsonObject &kwargs) +CmdResult announceSession( + Client *client, const QJsonArray &args, const QJsonObject &kwargs) { - if(args.size()!=1) - throw CmdError("Expected one argument: API URL"); + if(args.size() != 1) + return CmdResult::err("Expected one argument: API URL"); - QUrl apiUrl { args.at(0).toString() }; + QUrl apiUrl{args.at(0).toString()}; if(!apiUrl.isValid()) - throw CmdError("Invalid API URL"); + return CmdResult::err("Invalid API URL"); client->session()->makeAnnouncement(apiUrl, kwargs["private"].toBool()); + return CmdResult::ok(); } -void unlistSession(Client *client, const QJsonArray &args, const QJsonObject &kwargs) +CmdResult +unlistSession(Client *client, const QJsonArray &args, const QJsonObject &kwargs) { Q_UNUSED(kwargs); if(args.size() != 1) - throw CmdError("Expected one argument: API URL"); + return CmdResult::err("Expected one argument: API URL"); client->session()->unlistAnnouncement(args.at(0).toString()); + return CmdResult::ok(); } -void resetSession(Client *client, const QJsonArray &args, const QJsonObject &kwargs) +CmdResult +resetSession(Client *client, const QJsonArray &args, const QJsonObject &kwargs) { Q_UNUSED(args); Q_UNUSED(kwargs); if(client->session()->state() != Session::State::Running) - throw CmdError("Unable to reset in this state"); + return CmdResult::err("Unable to reset in this state"); client->session()->resetSession(client->id()); + return CmdResult::ok(); } -void setMute(Client *client, const QJsonArray &args, const QJsonObject &kwargs) +CmdResult +setMute(Client *client, const QJsonArray &args, const QJsonObject &kwargs) { Q_UNUSED(kwargs); if(args.size() != 2) - throw CmdError("Expected two arguments: userId true/false"); + return CmdResult::err("Expected two arguments: userId true/false"); - Client *c = _getClient(client->session(), args.at(0)); + Client *c; + CmdResult result = getClient(client->session(), args.at(0), c); + if(!result.success()) { + return result; + } const bool m = args.at(1).toBool(); if(c->isMuted() != m) { c->setMuted(m); client->session()->sendUpdatedMuteList(); if(m) - c->log(Log().about(Log::Level::Info, Log::Topic::Mute).message("Muted by " + client->username())); + c->log(Log() + .about(Log::Level::Info, Log::Topic::Mute) + .message("Muted by " + client->username())); else - c->log(Log().about(Log::Level::Info, Log::Topic::Unmute).message("Unmuted by " + client->username())); + c->log(Log() + .about(Log::Level::Info, Log::Topic::Unmute) + .message("Unmuted by " + client->username())); } + return CmdResult::ok(); } -void reportAbuse(Client *client, const QJsonArray &args, const QJsonObject &kwargs) +CmdResult +reportAbuse(Client *client, const QJsonArray &args, const QJsonObject &kwargs) { Q_UNUSED(args); @@ -253,65 +324,67 @@ void reportAbuse(Client *client, const QJsonArray &args, const QJsonObject &kwar const QString reason = kwargs["reason"].toString(); client->session()->sendAbuseReport(client, user, reason); + return CmdResult::ok(); } SrvCommandSet::SrvCommandSet() { - commands - << SrvCommand("ready-to-autoreset", readyToAutoReset) - << SrvCommand("init-begin", initBegin) - << SrvCommand("init-complete", initComplete) - << SrvCommand("init-cancel", initCancel) - << SrvCommand("sessionconf", sessionConf) - << SrvCommand("kick-user", kickUser, SrvCommand::DEPUTY) - << SrvCommand("gain-op", opWord, SrvCommand::NONOP) - - << SrvCommand("reset-session", resetSession) - << SrvCommand("kill-session", killSession, SrvCommand::MOD) - - << SrvCommand("announce-session", announceSession) - << SrvCommand("unlist-session", unlistSession) - - << SrvCommand("remove-ban", removeBan) - << SrvCommand("mute", setMute) - - << SrvCommand("report", reportAbuse, SrvCommand::NONOP) - ; + commands << SrvCommand("ready-to-autoreset", readyToAutoReset) + << SrvCommand("init-begin", initBegin) + << SrvCommand("init-complete", initComplete) + << SrvCommand("init-cancel", initCancel) + << SrvCommand("sessionconf", sessionConf) + << SrvCommand("kick-user", kickUser, SrvCommand::DEPUTY) + << SrvCommand("gain-op", opWord, SrvCommand::NONOP) + + << SrvCommand("reset-session", resetSession) + << SrvCommand("kill-session", killSession, SrvCommand::MOD) + + << SrvCommand("announce-session", announceSession) + << SrvCommand("unlist-session", unlistSession) + + << SrvCommand("remove-ban", removeBan) + << SrvCommand("mute", setMute) + + << SrvCommand("report", reportAbuse, SrvCommand::NONOP); } } // end of anonymous namespace -void handleClientServerCommand(Client *client, const QString &command, const QJsonArray &args, const QJsonObject &kwargs) +void handleClientServerCommand( + Client *client, const QString &command, const QJsonArray &args, + const QJsonObject &kwargs) { for(const SrvCommand &c : COMMANDS.commands) { if(c.name() == command) { if(c.mode() == SrvCommand::MOD && !client->isModerator()) { - client->sendDirectMessage(protocol::Command::error(command + ": Not a moderator")); + client->sendDirectMessage(net::ServerReply::makeCommandError( + command, QStringLiteral("Not a moderator"))); return; - } - else if(c.mode() == SrvCommand::OP && !client->isOperator()) { - client->sendDirectMessage(protocol::Command::error(command + ": Not a session owner")); + } else if(c.mode() == SrvCommand::OP && !client->isOperator()) { + client->sendDirectMessage(net::ServerReply::makeCommandError( + command, QStringLiteral("Not a session owner"))); return; - } - else if(c.mode() == SrvCommand::DEPUTY && !client->isOperator() && !client->isDeputy()) { - client->sendDirectMessage(protocol::Command::error(command + ": Not a session owner or a deputy")); + } else if( + c.mode() == SrvCommand::DEPUTY && !client->isOperator() && + !client->isDeputy()) { + client->sendDirectMessage(net::ServerReply::makeCommandError( + command, + QStringLiteral("Not a session owner or a deputy"))); return; } - try { - c.call(client, args, kwargs); - } catch(const CmdError &err) { - - protocol::ServerReply reply; - reply.type = protocol::ServerReply::ERROR; - reply.message = err.message(); - client->sendDirectMessage(protocol::Command::error(err.message())); + CmdResult result = c.call(client, args, kwargs); + if(!result.success()) { + client->sendDirectMessage(net::ServerReply::makeCommandError( + command, result.message())); } return; } } - client->sendDirectMessage(protocol::Command::error("Unknown command: " + command)); + client->sendDirectMessage( + net::ServerReply::makeCommandError(command, "Unknown command")); } } diff --git a/src/libserver/opcommands.h b/src/libserver/opcommands.h index 8f3e552ef1..4fee6ed9d6 100644 --- a/src/libserver/opcommands.h +++ b/src/libserver/opcommands.h @@ -1,5 +1,4 @@ // SPDX-License-Identifier: GPL-3.0-or-later - #ifndef OPCOMMANDS_H #define OPCOMMANDS_H @@ -16,7 +15,9 @@ class Client; * @param client * @param command */ -void handleClientServerCommand(Client *client, const QString &command, const QJsonArray &args, const QJsonObject &kwargs); +void handleClientServerCommand( + Client *client, const QString &command, const QJsonArray &args, + const QJsonObject &kwargs); } diff --git a/src/libserver/session.cpp b/src/libserver/session.cpp index e014b32d40..8363112dcd 100644 --- a/src/libserver/session.cpp +++ b/src/libserver/session.cpp @@ -1,30 +1,28 @@ // SPDX-License-Identifier: GPL-3.0-or-later - -#include "libserver/session.h" +extern "C" { +#include +#include +} +#include "libserver/announcements.h" #include "libserver/client.h" +#include "libserver/opcommands.h" #include "libserver/serverconfig.h" #include "libserver/serverlog.h" -#include "libserver/opcommands.h" -#include "libserver/announcements.h" - -#include "libshared/net/control.h" -#include "libshared/net/meta.h" -#include "libshared/record/writer.h" +#include "libserver/session.h" +#include "libshared/net/servercmd.h" #include "libshared/util/filename.h" -#include "libshared/util/passwordhash.h" #include "libshared/util/networkaccess.h" - -#include -#include +#include "libshared/util/passwordhash.h" #include +#include +#include namespace server { -using protocol::MessagePtr; - static bool forceEnableNsfm(SessionHistory *history, const ServerConfig *config) { - if(config->getConfigBool(config::ForceNsfm) && !history->hasFlag(SessionHistory::Nsfm)) { + if(config->getConfigBool(config::ForceNsfm) && + !history->hasFlag(SessionHistory::Nsfm)) { SessionHistory::Flags flags = history->flags(); flags.setFlag(SessionHistory::Nsfm); history->setFlags(flags); @@ -34,75 +32,80 @@ static bool forceEnableNsfm(SessionHistory *history, const ServerConfig *config) } } -Session::Session(SessionHistory *history, ServerConfig *config, sessionlisting::Announcements *announcements, QObject *parent) - : QObject(parent), - m_history(history), - m_config(config), - m_announcements(announcements) +Session::Session( + SessionHistory *history, ServerConfig *config, + sessionlisting::Announcements *announcements, QObject *parent) + : QObject(parent) + , m_history(history) + , m_config(config) + , m_announcements(announcements) { m_history->setParent(this); - connect(m_config, &ServerConfig::configValueChanged, this, &Session::onConfigValueChanged); + connect( + m_config, &ServerConfig::configValueChanged, this, + &Session::onConfigValueChanged); forceEnableNsfm(m_history, m_config); m_lastEventTime.start(); // History already exists? Skip the Initialization state. - if(history->sizeInBytes()>0) + if(history->sizeInBytes() > 0) m_state = State::Running; // Session announcements - connect(m_announcements, &sessionlisting::Announcements::announcementsChanged, this, &Session::onAnnouncementsChanged); - connect(m_announcements, &sessionlisting::Announcements::announcementError, this, &Session::onAnnouncementError); + connect( + m_announcements, &sessionlisting::Announcements::announcementsChanged, + this, &Session::onAnnouncementsChanged); + connect( + m_announcements, &sessionlisting::Announcements::announcementError, + this, &Session::onAnnouncementError); for(const QString &announcement : m_history->announcements()) makeAnnouncement(QUrl(announcement), false); } -static protocol::MessagePtr makeLogMessage(const Log &log) +static net::Message makeLogMessage(const Log &log) { - protocol::ServerReply sr { - protocol::ServerReply::LOG, - log.message(), - log.toJson(Log::NoPrivateData|Log::NoSession) - }; - return protocol::MessagePtr(new protocol::Command(0, sr)); + return net::ServerReply::makeLog( + log.message(), log.toJson(Log::NoPrivateData | Log::NoSession)); } -protocol::MessageList Session::serverSideStateMessages() const +net::MessageList Session::serverSideStateMessages() const { - protocol::MessageList msgs; - - QList owners; - QList trusted; + net::MessageList msgs; + QVector owners; + QVector trusted; for(const Client *c : m_clients) { - msgs << c->joinMessage(); - if(c->isOperator()) - owners << c->id(); - if(c->isTrusted()) - trusted << c->id(); + msgs.append(c->joinMessage()); + if(c->isOperator()) { + owners.append(c->id()); + } + if(c->isTrusted()) { + trusted.append(c->id()); + } } - msgs << protocol::MessagePtr(new protocol::SessionOwner(0, owners)); - - if(!trusted.isEmpty()) - msgs << protocol::MessagePtr(new protocol::TrustedUsers(0, trusted)); + msgs.append(net::makeSessionOwnerMessage(0, owners)); + if(!trusted.isEmpty()) { + msgs.append(net::makeTrustedUsersMessage(0, trusted)); + } return msgs; } void Session::switchState(State newstate) { - if(newstate==State::Initialization) { + if(newstate == State::Initialization) { qFatal("Illegal state change to Initialization from %d", int(m_state)); - } else if(newstate==State::Running) { - if(m_state!=State::Initialization && m_state!=State::Reset) + } else if(newstate == State::Running) { + if(m_state != State::Initialization && m_state != State::Reset) qFatal("Illegal state change to Running from %d", int(m_state)); m_initUser = -1; bool success = true; - if(m_state==State::Reset && !m_resetstream.isEmpty()) { + if(m_state == State::Reset && !m_resetstream.isEmpty()) { // Reset buffer uploaded. Now perform the reset before returning to // normal running state. @@ -110,24 +113,21 @@ void Session::switchState(State newstate) // Send reset snapshot if(!m_history->reset(resetImage)) { - // This shouldn't normally happen, as the size limit should be caught while - // still uploading the reset. + // This shouldn't normally happen, as the size limit should be + // caught while still uploading the reset. messageAll("Session reset failed!", true); success = false; } else { - protocol::ServerReply resetcmd; - resetcmd.type = protocol::ServerReply::RESET; - resetcmd.reply["state"] = "reset"; - resetcmd.message = "Session reset!"; - directToAll(MessagePtr(new protocol::Command(0, resetcmd))); + directToAll(net::ServerReply::makeReset( + QStringLiteral("Session reset!"), QStringLiteral("reset"))); onSessionReset(); sendUpdatedSessionProperties(); } - m_resetstream = protocol::MessageList(); + m_resetstream = net::MessageList{}; m_resetstreamsize = 0; } @@ -137,9 +137,10 @@ void Session::switchState(State newstate) for(Client *c : m_clients) c->setHoldLocked(false); - } else if(newstate==State::Reset) { - if(m_state!=State::Running) + } else if(newstate == State::Reset) { + if(m_state != State::Running) { qFatal("Illegal state change to Reset from %d", int(m_state)); + } m_resetstream.clear(); m_resetstreamsize = 0; @@ -153,12 +154,13 @@ void Session::assignId(Client *user) { uint8_t id = m_history->idQueue().getIdForName(user->username()); - int loops=256; - while(loops>0 && (id==0 || getClientById(id))) { + int loops = 256; + while(loops > 0 && (id == 0 || getClientById(id))) { id = m_history->idQueue().nextId(); - --loops; + --loops; } - Q_ASSERT(loops>0); // shouldn't happen, since we don't let new users in if the session is full + Q_ASSERT(loops > 0); // shouldn't happen, since we don't let new users in if + // the session is full user->setId(id); } @@ -173,10 +175,15 @@ void Session::joinUser(Client *user, bool host) // Send session log history to the new client { - QList log = m_config->logger()->query().session(id()).atleast(Log::Level::Info).get(); + QList log = m_config->logger() + ->query() + .session(id()) + .atleast(Log::Level::Info) + .get(); // Note: the query returns the log entries in latest first, but we send - // new entries to clients as they occur, so we reverse the list before sending it - for(int i=log.size()-1;i>=0;--i) { + // new entries to clients as they occur, so we reverse the list before + // sending it + for(int i = log.size() - 1; i >= 0; --i) { user->sendDirectMessage(makeLogMessage(log.at(i))); } } @@ -197,12 +204,14 @@ void Session::joinUser(Client *user, bool host) if(user->isOperator() || m_history->isOperator(user->authId())) changeOpStatus(user->id(), true, "the server"); - if(user->authFlags().contains("TRUSTED") || m_history->isTrusted(user->authId())) + if(user->authFlags().contains("TRUSTED") || + m_history->isTrusted(user->authId())) changeTrustedStatus(user->id(), true, "the server"); ensureOperatorExists(); - const QString welcomeMessage = m_config->getConfigString(config::WelcomeMessage); + const QString welcomeMessage = + m_config->getConfigString(config::WelcomeMessage); if(!welcomeMessage.isEmpty()) { user->sendSystemChat(welcomeMessage); } @@ -214,7 +223,9 @@ void Session::joinUser(Client *user, bool host) m_history->joinUser(user->id(), user->username()); - user->log(Log().about(Log::Level::Info, Log::Topic::Join).message("Joined session")); + user->log(Log() + .about(Log::Level::Info, Log::Topic::Join) + .message("Joined session")); emit sessionAttributeChanged(this); } @@ -223,16 +234,15 @@ void Session::removeUser(Client *user) if(!m_clients.removeOne(user)) return; - m_pastClients.insert(user->id(), PastClient { - user->id(), - user->authId(), - user->username(), - user->peerAddress(), - !user->isModerator() - }); + m_pastClients.insert( + user->id(), PastClient{ + user->id(), user->authId(), user->username(), + user->peerAddress(), !user->isModerator()}); Q_ASSERT(user->session() == this); - user->log(Log().about(Log::Level::Info, Log::Topic::Leave).message("Left session")); + user->log(Log() + .about(Log::Level::Info, Log::Topic::Leave) + .message("Left session")); user->setSession(nullptr); disconnect(user, nullptr, this, nullptr); @@ -244,8 +254,9 @@ void Session::removeUser(Client *user) abortReset(); } - addToHistory(MessagePtr(new protocol::UserLeave(user->id()))); - m_history->idQueue().reserveId(user->id()); // Try not to reuse the ID right away + addToHistory(net::makeLeaveMessage(user->id())); + // Try not to reuse the ID right away + m_history->idQueue().reserveId(user->id()); ensureOperatorExists(); @@ -269,8 +280,9 @@ void Session::abortReset() Client *Session::getClientById(uint8_t id) { for(Client *c : m_clients) { - if(c->id() == id) + if(c->id() == id) { return c; + } } return nullptr; } @@ -278,8 +290,9 @@ Client *Session::getClientById(uint8_t id) Client *Session::getClientByUsername(const QString &username) { for(Client *c : m_clients) { - if(c->username().compare(username, Qt::CaseInsensitive)==0) + if(c->username().compare(username, Qt::CaseInsensitive) == 0) { return c; + } } return nullptr; } @@ -287,17 +300,25 @@ Client *Session::getClientByUsername(const QString &username) void Session::addBan(const Client *target, const QString &bannedBy) { Q_ASSERT(target); - if(m_history->addBan(target->username(), target->peerAddress(), target->authId(), bannedBy)) { - target->log(Log().about(Log::Level::Info, Log::Topic::Ban).message("Banned by " + bannedBy)); + if(m_history->addBan( + target->username(), target->peerAddress(), target->authId(), + bannedBy)) { + target->log(Log() + .about(Log::Level::Info, Log::Topic::Ban) + .message("Banned by " + bannedBy)); sendUpdatedBanlist(); } } void Session::addBan(const PastClient &target, const QString &bannedBy) { - Q_ASSERT(target.id>0); - if(m_history->addBan(target.username, target.peerAddress, target.authId, bannedBy)) { - log(Log().user(target.id, target.peerAddress, target.username).about(Log::Level::Info, Log::Topic::Ban).message("Banned by " + bannedBy)); + Q_ASSERT(target.id > 0); + if(m_history->addBan( + target.username, target.peerAddress, target.authId, bannedBy)) { + log(Log() + .user(target.id, target.peerAddress, target.username) + .about(Log::Level::Info, Log::Topic::Ban) + .message("Banned by " + bannedBy)); sendUpdatedBanlist(); } } @@ -306,16 +327,17 @@ void Session::removeBan(int entryId, const QString &removedBy) { QString unbanned = m_history->removeBan(entryId); if(!unbanned.isEmpty()) { - log(Log().about(Log::Level::Info, Log::Topic::Unban).message(unbanned + " unbanned by " + removedBy)); + log(Log() + .about(Log::Level::Info, Log::Topic::Unban) + .message(unbanned + " unbanned by " + removedBy)); sendUpdatedBanlist(); } } bool Session::isClosed() const { - return m_closed - || userCount() >= m_history->maxUsers() - || (m_state != State::Initialization && m_state != State::Running); + return m_closed || userCount() >= m_history->maxUsers() || + (m_state != State::Initialization && m_state != State::Running); } void Session::setClosed(bool closed) @@ -340,16 +362,24 @@ void Session::setSessionConfig(const QJsonObject &conf, Client *changedBy) if(conf.contains("authOnly")) { const bool authOnly = conf["authOnly"].toBool(); // The authOnly flag can only be set by an authenticated user. - // Otherwise it would be possible for users to accidentally lock themselves out. + // Otherwise it would be possible for users to accidentally lock + // themselves out. if(!authOnly || !changedBy || changedBy->isAuthenticated()) { flags.setFlag(SessionHistory::AuthOnly, authOnly); - changes << (authOnly ? "blocked guest logins" : "permitted guest logins"); + changes + << (authOnly ? "blocked guest logins" + : "permitted guest logins"); } } if(conf.contains("persistent")) { - flags.setFlag(SessionHistory::Persistent, conf["persistent"].toBool() && m_config->getConfigBool(config::EnablePersistence)); - changes << (conf["persistent"].toBool() ? "made persistent" : "made nonpersistent"); + flags.setFlag( + SessionHistory::Persistent, + conf["persistent"].toBool() && + m_config->getConfigBool(config::EnablePersistence)); + changes + << (conf["persistent"].toBool() ? "made persistent" + : "made nonpersistent"); } if(conf.contains("title")) { @@ -367,7 +397,8 @@ void Session::setSessionConfig(const QJsonObject &conf, Client *changedBy) if(conf["resetThreshold"].isDouble()) val = conf["resetThreshold"].toInt(); else - val = ServerConfig::parseSizeString(conf["resetThreshold"].toString()); + val = ServerConfig::parseSizeString( + conf["resetThreshold"].toString()); m_history->setAutoResetThreshold(val); changes << "changed autoreset threshold"; } @@ -386,8 +417,11 @@ void Session::setSessionConfig(const QJsonObject &conf, Client *changedBy) // the client whether to send preserved/recorded chat messages // by default. if(conf.contains("preserveChat")) { - flags.setFlag(SessionHistory::PreserveChat, conf["preserveChat"].toBool()); - changes << (conf["preserveChat"].toBool() ? "preserve chat" : "don't preserve chat"); + flags.setFlag( + SessionHistory::PreserveChat, conf["preserveChat"].toBool()); + changes + << (conf["preserveChat"].toBool() ? "preserve chat" + : "don't preserve chat"); } if(m_config->getConfigBool(config::ForceNsfm)) { @@ -402,7 +436,9 @@ void Session::setSessionConfig(const QJsonObject &conf, Client *changedBy) if(conf.contains("deputies")) { flags.setFlag(SessionHistory::Deputies, conf["deputies"].toBool()); - changes << (conf["deputies"].toBool() ? "enabled deputies" : "disabled deputies"); + changes + << (conf["deputies"].toBool() ? "enabled deputies" + : "disabled deputies"); } m_history->setFlags(flags); @@ -412,7 +448,8 @@ void Session::setSessionConfig(const QJsonObject &conf, Client *changedBy) QString logmsg = changes.join(", "); logmsg[0] = logmsg[0].toUpper(); - Log l = Log().about(Log::Level::Info, Log::Topic::Status).message(logmsg); + Log l = + Log().about(Log::Level::Info, Log::Topic::Status).message(logmsg); if(changedBy) changedBy->log(l); else @@ -420,18 +457,19 @@ void Session::setSessionConfig(const QJsonObject &conf, Client *changedBy) } } -QList Session::updateOwnership(QList ids, const QString &changedBy) +QVector +Session::updateOwnership(QVector ids, const QString &changedBy) { - QList truelist; + QVector truelist; Client *kickResetter = nullptr; for(Client *c : m_clients) { const bool op = ids.contains(c->id()) || c->isModerator(); if(op != c->isOperator()) { if(!op && c->id() == m_initUser && m_state == State::Reset) { - // OP status removed mid-reset! The user probably has at least part - // of the reset image still queued for upload, which will messs up - // the session once we're out of reset mode. Kicking the client - // is the easiest workaround. + // OP status removed mid-reset! The user probably has at least + // part of the reset image still queued for upload, which will + // messs up the session once we're out of reset mode. Kicking + // the client is the easiest workaround. // TODO for 3.0: send a cancel command to the client and ignore // all further input until ack is received. kickResetter = c; @@ -441,254 +479,286 @@ QList Session::updateOwnership(QList ids, const QString &chang QString msg; if(op) { msg = "Made operator by " + changedBy; - c->log(Log().about(Log::Level::Info, Log::Topic::Op).message(msg)); + c->log( + Log().about(Log::Level::Info, Log::Topic::Op).message(msg)); } else { msg = "Operator status revoked by " + changedBy; - c->log(Log().about(Log::Level::Info, Log::Topic::Deop).message(msg)); + c->log(Log() + .about(Log::Level::Info, Log::Topic::Deop) + .message(msg)); } messageAll(c->username() + " " + msg, false); - if(c->isAuthenticated() && !c->isModerator()) + if(c->isAuthenticated() && !c->isModerator()) { m_history->setAuthenticatedOperator(c->authId(), op); - + } + } + if(c->isOperator()) { + truelist.append(c->id()); } - if(c->isOperator()) - truelist << c->id(); } - if(kickResetter) - kickResetter->disconnectClient(Client::DisconnectionReason::Error, "De-opped while resetting"); + if(kickResetter) { + kickResetter->disconnectClient( + Client::DisconnectionReason::Error, "De-opped while resetting"); + } return truelist; } void Session::changeOpStatus(uint8_t id, bool op, const QString &changedBy) { - QList ids; + QVector ids; for(const Client *c : m_clients) { - if(c->isOperator()) - ids << c->id(); + if(c->isOperator()) { + ids.append(c->id()); + } } - if(op) - ids << id; - else + if(op) { + ids.append(id); + } else { ids.removeOne(id); + } ids = updateOwnership(ids, changedBy); - addToHistory(protocol::MessagePtr(new protocol::SessionOwner(0, ids))); + addToHistory(net::makeSessionOwnerMessage(0, ids)); } -QList Session::updateTrustedUsers(QList ids, const QString &changedBy) +QVector +Session::updateTrustedUsers(QVector ids, const QString &changedBy) { - QList truelist; + QVector truelist; for(Client *c : m_clients) { - const bool trusted = ids.contains(c->id()); + bool trusted = ids.contains(c->id()); if(trusted != c->isTrusted()) { c->setTrusted(trusted); QString msg; if(trusted) { msg = "Trusted by " + changedBy; - c->log(Log().about(Log::Level::Info, Log::Topic::Trust).message(msg)); + c->log(Log() + .about(Log::Level::Info, Log::Topic::Trust) + .message(msg)); } else { msg = "Untrusted by " + changedBy; - c->log(Log().about(Log::Level::Info, Log::Topic::Untrust).message(msg)); + c->log(Log() + .about(Log::Level::Info, Log::Topic::Untrust) + .message(msg)); } messageAll(c->username() + " " + msg, false); - if(c->isAuthenticated()) + if(c->isAuthenticated()) { m_history->setAuthenticatedTrust(c->authId(), trusted); - + } + } + if(c->isTrusted()) { + truelist.append(c->id()); } - if(c->isTrusted()) - truelist << c->id(); } return truelist; } -void Session::changeTrustedStatus(uint8_t id, bool trusted, const QString &changedBy) +void Session::changeTrustedStatus( + uint8_t id, bool trusted, const QString &changedBy) { - QList ids; + QVector ids; for(const Client *c : m_clients) { - if(c->isTrusted()) - ids << c->id(); + if(c->isTrusted()) { + ids.append(c->id()); + } } - if(trusted) - ids << id; - else + if(trusted) { + ids.append(id); + } else { ids.removeOne(id); + } ids = updateTrustedUsers(ids, changedBy); - addToHistory(protocol::MessagePtr(new protocol::TrustedUsers(0, ids))); + addToHistory(net::makeTrustedUsersMessage(0, ids)); } void Session::sendUpdatedSessionProperties() { - protocol::ServerReply props; - props.type = protocol::ServerReply::SESSIONCONF; - QJsonObject conf; - conf["closed"] = m_closed; // this refers specifically to the closed flag, not the general status - conf["authOnly"] = m_history->hasFlag(SessionHistory::AuthOnly); - conf["persistent"] = m_history->hasFlag(SessionHistory::Persistent); - conf["title"] = m_history->title(); - conf["maxUserCount"] = m_history->maxUsers(); - conf["resetThreshold"] = int(m_history->autoResetThreshold()); - conf["resetThresholdBase"] = int(m_history->autoResetThresholdBase()); - conf["preserveChat"] = m_history->flags().testFlag(SessionHistory::PreserveChat); - conf["nsfm"] = m_history->flags().testFlag(SessionHistory::Nsfm); - conf["deputies"] = m_history->flags().testFlag(SessionHistory::Deputies); - conf["hasPassword"] = !m_history->passwordHash().isEmpty(); - conf["hasOpword"] = !m_history->opwordHash().isEmpty(); - // This config option is basically a session property set by the server. - // We report it here so the client can disable the checkbox in the UI. - conf["forceNsfm"] = m_config->getConfigBool(config::ForceNsfm); - props.reply["config"] = conf; - - addToHistory(protocol::MessagePtr(new protocol::Command(0, props))); + addToHistory(net::ServerReply::makeSessionConf({ + // this refers specifically to the closed flag, not the general status + {QStringLiteral("closed"), m_closed}, + {QStringLiteral("authOnly"), + m_history->hasFlag(SessionHistory::AuthOnly)}, + {QStringLiteral("persistent"), + m_history->hasFlag(SessionHistory::Persistent)}, + {QStringLiteral("title"), m_history->title()}, + {QStringLiteral("maxUserCount"), m_history->maxUsers()}, + {QStringLiteral("resetThreshold"), + int(m_history->autoResetThreshold())}, + {QStringLiteral("resetThresholdBase"), + int(m_history->autoResetThresholdBase())}, + {QStringLiteral("preserveChat"), + m_history->flags().testFlag(SessionHistory::PreserveChat)}, + {QStringLiteral("nsfm"), + m_history->flags().testFlag(SessionHistory::Nsfm)}, + {QStringLiteral("deputies"), + m_history->flags().testFlag(SessionHistory::Deputies)}, + {QStringLiteral("hasPassword"), !m_history->passwordHash().isEmpty()}, + {QStringLiteral("hasOpword"), !m_history->opwordHash().isEmpty()}, + // This config option is basically a session property set by the server. + // We report it here so the client can disable the checkbox in the UI. + {QStringLiteral("forceNsfm"), + m_config->getConfigBool(config::ForceNsfm)}, + })); emit sessionAttributeChanged(this); } void Session::sendUpdatedBanlist() { - // The banlist is not usually included in the sessionconf. - // Moderators and local users get to see the actual IP addresses too - protocol::ServerReply msg; - msg.type = protocol::ServerReply::SESSIONCONF; - QJsonObject conf; - conf["banlist"] = m_history->banlist().toJson(false); - msg.reply["config"] = conf; - // Normal users don't get to see the actual IP addresses - const protocol::MessagePtr normalVersion(new protocol::Command(0, msg)); + net::Message normalVersion(net::ServerReply::makeSessionConf( + {{QStringLiteral("banlist"), m_history->banlist().toJson(false)}})); // But moderators and local users do - conf["banlist"] = m_history->banlist().toJson(true); - msg.reply["config"] = conf; - const protocol::MessagePtr modVersion(new protocol::Command(0, msg)); + net::Message modVersion(net::ServerReply::makeSessionConf( + {{QStringLiteral("banlist"), m_history->banlist().toJson(true)}})); for(Client *c : m_clients) { - if(c->isModerator() || c->peerAddress().isLoopback()) + if(c->isModerator() || c->peerAddress().isLoopback()) { c->sendDirectMessage(modVersion); - else + } else { c->sendDirectMessage(normalVersion); + } } } void Session::sendUpdatedAnnouncementList() { // The announcement list is not usually included in the sessionconf. - protocol::ServerReply msg; - msg.type = protocol::ServerReply::SESSIONCONF; - QJsonArray list; - const auto announcements = m_announcements->getAnnouncements(this); - for(const sessionlisting::Announcement &a : announcements) { - QJsonObject o; - o["url"] = a.apiUrl.toString(); - o["roomcode"] = a.roomcode; - o["private"] = a.isPrivate; - list.append(o); + QJsonArray announcements; + for(const sessionlisting::Announcement &a : + m_announcements->getAnnouncements(this)) { + announcements.append(QJsonObject{ + {QStringLiteral("url"), a.apiUrl.toString()}, + {QStringLiteral("roomcode"), a.roomcode}, + {QStringLiteral("private"), a.isPrivate}, + }); } - - QJsonObject conf; - conf["announcements"]= list; - msg.reply["config"] = conf; - directToAll(protocol::MessagePtr(new protocol::Command(0, msg))); + directToAll(net::ServerReply::makeSessionConf( + {{QStringLiteral("announcements"), announcements}})); } void Session::sendUpdatedMuteList() { // The mute list is not usually included in the sessionconf. - protocol::ServerReply msg; - msg.type = protocol::ServerReply::SESSIONCONF; QJsonArray muted; for(const Client *c : m_clients) { - if(c->isMuted()) + if(c->isMuted()) { muted.append(c->id()); + } } - - QJsonObject conf; - conf["muted"]= muted; - msg.reply["config"] = conf; - directToAll(protocol::MessagePtr(new protocol::Command(0, msg))); + directToAll( + net::ServerReply::makeSessionConf({{QStringLiteral("muted"), muted}})); } -void Session::handleClientMessage(Client &client, protocol::MessagePtr msg) +void Session::handleClientMessage(Client &client, const net::Message &msg) { // Filter away server-to-client-only messages - switch(msg->type()) { - using namespace protocol; - case MSG_USER_JOIN: - case MSG_USER_LEAVE: - case MSG_SOFTRESET: - client.log(Log().about(Log::Level::Warn, Log::Topic::RuleBreak).message("Received server-to-user only command " + msg->messageName())); + switch(msg.type()) { + case DP_MSG_JOIN: + case DP_MSG_LEAVE: + case DP_MSG_SOFT_RESET: + client.log( + Log() + .about(Log::Level::Warn, Log::Topic::RuleBreak) + .message( + "Received server-to-user only command " + msg.typeName())); return; - case MSG_DISCONNECT: + case DP_MSG_DISCONNECT: // we don't do anything with disconnect notifications from the client return; - default: break; + default: + break; } // Some meta commands affect the server too - switch(msg->type()) { - case protocol::MSG_COMMAND: { - protocol::ServerCommand cmd = msg.cast().cmd(); - handleClientServerCommand(&client, cmd.cmd, cmd.args, cmd.kwargs); + switch(msg.type()) { + case DP_MSG_SERVER_COMMAND: { + net::ServerCommand cmd = net::ServerCommand::fromMessage(msg); + handleClientServerCommand(&client, cmd.cmd, cmd.args, cmd.kwargs); + return; + } + case DP_MSG_SESSION_OWNER: { + if(!client.isOperator()) { + client.log(Log() + .about(Log::Level::Warn, Log::Topic::RuleBreak) + .message("Tried to change session ownership")); return; } - case protocol::MSG_SESSION_OWNER: { - if(!client.isOperator()) { - client.log(Log().about(Log::Level::Warn, Log::Topic::RuleBreak).message("Tried to change session ownership")); - return; - } - QList ids = msg.cast().ids(); - ids.append(client.id()); - ids = updateOwnership(ids, client.username()); - msg.cast().setIds(ids); - break; + int count; + const uint8_t *users = + DP_msg_session_owner_users(msg.toSessionOwner(), &count); + QVector ids{users, users + count}; + ids.append(client.id()); + addClientMessage( + client, + net::makeSessionOwnerMessage( + msg.contextId(), updateOwnership(ids, client.username()))); + return; + } + case DP_MSG_CHAT: { + if(client.isMuted()) { + return; } - case protocol::MSG_CHAT: { - if(client.isMuted()) - return; - if(msg.cast().isBypass()) { - directToAll(msg); - return; - } - break; + if(DP_msg_chat_tflags(msg.toChat()) & DP_MSG_CHAT_TFLAGS_BYPASS) { + directToAll(msg); + return; } - case protocol::MSG_PRIVATE_CHAT: { - const protocol::PrivateChat &chat = msg.cast(); - if(chat.target()>0) { - Client *target = getClientById(chat.target()); - if(target) { - client.sendDirectMessage(msg); - target->sendDirectMessage(msg); - } + break; + } + case DP_MSG_PRIVATE_CHAT: { + uint8_t targetId = DP_msg_private_chat_target(msg.toPrivateChat()); + if(targetId > 0) { + Client *target = getClientById(targetId); + if(target) { + client.sendDirectMessage(msg); + target->sendDirectMessage(msg); } + } + return; + } + case DP_MSG_TRUSTED_USERS: { + if(!client.isOperator()) { + log(Log() + .about(Log::Level::Warn, Log::Topic::RuleBreak) + .message("Tried to change trusted user list")); return; } - case protocol::MSG_TRUSTED_USERS: { - if(!client.isOperator()) { - log(Log().about(Log::Level::Warn, Log::Topic::RuleBreak).message("Tried to change trusted user list")); - return; - } - QList ids = msg.cast().ids(); - ids = updateTrustedUsers(ids, client.username()); - msg.cast().setIds(ids); - break; - } - default: break; + int count; + const uint8_t *users = + DP_msg_trusted_users_users(msg.toTrustedUsers(), &count); + QVector ids{users, users + count}; + addClientMessage( + client, + net::makeTrustedUsersMessage( + msg.contextId(), updateTrustedUsers(ids, client.username()))); + return; + } + default: + break; } // Rest of the messages are added to session history - if(initUserId() == client.id()) + addClientMessage(client, msg); +} + +void Session::addClientMessage(const Client &client, const net::Message &msg) +{ + if(initUserId() == client.id()) { addToInitStream(msg); - else + } else { addToHistory(msg); + } } -void Session::addToInitStream(protocol::MessagePtr msg) +void Session::addToInitStream(const net::Message &msg) { Q_ASSERT(m_state != State::Running); @@ -696,14 +766,18 @@ void Session::addToInitStream(protocol::MessagePtr msg) addToHistory(msg); } else if(m_state == State::Reset) { - m_resetstreamsize += msg->length(); + m_resetstreamsize += msg.length(); m_resetstream.append(msg); - // Well behaved clients should be aware of the history limit and not exceed it. - if(m_history->sizeLimit()>0 && m_resetstreamsize > m_history->sizeLimit()) { + // Well behaved clients should be aware of the history limit and not + // exceed it. + if(m_history->sizeLimit() > 0 && + m_resetstreamsize > m_history->sizeLimit()) { Client *resetter = getClientById(m_initUser); if(resetter) - resetter->disconnectClient(Client::DisconnectionReason::Error, "History limit exceeded"); + resetter->disconnectClient( + Client::DisconnectionReason::Error, + "History limit exceeded"); } } } @@ -713,23 +787,36 @@ void Session::handleInitBegin(int ctxId) Client *c = getClientById(ctxId); if(!c) { // Shouldn't happen - log(Log().about(Log::Level::Error, Log::Topic::RuleBreak).message(QString("Non-existent user %1 sent init-begin").arg(ctxId))); + log(Log() + .about(Log::Level::Error, Log::Topic::RuleBreak) + .message(QString("Non-existent user %1 sent init-begin") + .arg(ctxId))); return; } if(ctxId != m_initUser) { - c->log(Log().about(Log::Level::Warn, Log::Topic::RuleBreak).message(QString("Sent init-begin, but init user is #%1").arg(m_initUser))); + c->log(Log() + .about(Log::Level::Warn, Log::Topic::RuleBreak) + .message(QString("Sent init-begin, but init user is #%1") + .arg(m_initUser))); return; } - c->log(Log().about(Log::Level::Debug, Log::Topic::Status).message("init-begin")); - - // It's possible that regular non-reset commands were still in the upload buffer - // when the client started sending the reset snapshot. The init-begin indicates - // the start of the true reset snapshot, so we can clear out the buffer here. - // For backward-compatibility, sending the init-begin command is optional. - if(m_resetstreamsize>0) { - c->log(Log().about(Log::Level::Debug, Log::Topic::Status).message(QStringLiteral("%1 extra messages cleared by init-begin").arg(m_resetstream.size()))); + c->log(Log() + .about(Log::Level::Debug, Log::Topic::Status) + .message("init-begin")); + + // It's possible that regular non-reset commands were still in the upload + // buffer when the client started sending the reset snapshot. The init-begin + // indicates the start of the true reset snapshot, so we can clear out the + // buffer here. For backward-compatibility, sending the init-begin command + // is optional. + if(m_resetstreamsize > 0) { + c->log(Log() + .about(Log::Level::Debug, Log::Topic::Status) + .message( + QStringLiteral("%1 extra messages cleared by init-begin") + .arg(m_resetstream.size()))); m_resetstream.clear(); m_resetstreamsize = 0; } @@ -740,16 +827,24 @@ void Session::handleInitComplete(int ctxId) Client *c = getClientById(ctxId); if(!c) { // Shouldn't happen - log(Log().about(Log::Level::Error, Log::Topic::RuleBreak).message(QString("Non-existent user %1 sent init-complete").arg(ctxId))); + log(Log() + .about(Log::Level::Error, Log::Topic::RuleBreak) + .message(QString("Non-existent user %1 sent init-complete") + .arg(ctxId))); return; } if(ctxId != m_initUser) { - c->log(Log().about(Log::Level::Warn, Log::Topic::RuleBreak).message(QString("Sent init-complete, but init user is #%1").arg(m_initUser))); + c->log(Log() + .about(Log::Level::Warn, Log::Topic::RuleBreak) + .message(QString("Sent init-complete, but init user is #%1") + .arg(m_initUser))); return; } - c->log(Log().about(Log::Level::Debug, Log::Topic::Status).message("init-complete")); + c->log(Log() + .about(Log::Level::Debug, Log::Topic::Status) + .message("init-complete")); switchState(State::Running); } @@ -760,16 +855,24 @@ void Session::handleInitCancel(int ctxId) Client *c = getClientById(ctxId); if(!c) { // Shouldn't happen - log(Log().about(Log::Level::Error, Log::Topic::RuleBreak).message(QString("Non-existent user %1 sent init-complete").arg(ctxId))); + log(Log() + .about(Log::Level::Error, Log::Topic::RuleBreak) + .message(QString("Non-existent user %1 sent init-complete") + .arg(ctxId))); return; } if(ctxId != m_initUser) { - c->log(Log().about(Log::Level::Warn, Log::Topic::RuleBreak).message(QString("Sent init-cancel, but init user is #%1").arg(m_initUser))); + c->log(Log() + .about(Log::Level::Warn, Log::Topic::RuleBreak) + .message(QString("Sent init-cancel, but init user is #%1") + .arg(m_initUser))); return; } - c->log(Log().about(Log::Level::Debug, Log::Topic::Status).message("init-cancel")); + c->log(Log() + .about(Log::Level::Debug, Log::Topic::Status) + .message("init-cancel")); abortReset(); } @@ -781,12 +884,9 @@ void Session::resetSession(int resetter) m_initUser = resetter; switchState(State::Reset); - protocol::ServerReply resetRequest; - resetRequest.type = protocol::ServerReply::RESET; - resetRequest.reply["state"] = "init"; - resetRequest.message = "Prepared to receive session data"; - - getClientById(resetter)->sendDirectMessage(protocol::MessagePtr(new protocol::Command(0, resetRequest))); + getClientById(resetter)->sendDirectMessage(net::ServerReply::makeReset( + QStringLiteral("Prepared to receive session data"), + QStringLiteral("init"))); } void Session::killSession(bool terminate) @@ -799,7 +899,8 @@ void Session::killSession(bool terminate) stopRecording(); for(Client *c : m_clients) { - c->disconnectClient(Client::DisconnectionReason::Shutdown, "Session terminated"); + c->disconnectClient( + Client::DisconnectionReason::Shutdown, "Session terminated"); c->setSession(nullptr); } m_clients.clear(); @@ -810,7 +911,7 @@ void Session::killSession(bool terminate) this->deleteLater(); } -void Session::directToAll(protocol::MessagePtr msg) +void Session::directToAll(const net::Message &msg) { for(Client *c : m_clients) { c->sendDirectMessage(msg); @@ -819,29 +920,25 @@ void Session::directToAll(protocol::MessagePtr msg) void Session::messageAll(const QString &message, bool alert) { - if(message.isEmpty()) - return; - - directToAll(protocol::MessagePtr(new protocol::Command(0, - (protocol::ServerReply { - alert ? protocol::ServerReply::ALERT : protocol::ServerReply::MESSAGE, - message, - QJsonObject() - }).toJson())) - ); + if(!message.isEmpty()) { + directToAll( + alert ? net::ServerReply::makeAlert(message) + : net::ServerReply::makeMessage(message)); + } } void Session::ensureOperatorExists() { // If there is a way to gain OP status without being explicitly granted, // it's OK for the session to not have any operators for a while. - if(!m_history->opwordHash().isEmpty() || m_history->isAuthenticatedOperators()) + if(!m_history->opwordHash().isEmpty() || + m_history->isAuthenticatedOperators()) return; - bool hasOp=false; + bool hasOp = false; for(const Client *c : m_clients) { if(c->isOperator()) { - hasOp=true; + hasOp = true; break; } } @@ -851,11 +948,14 @@ void Session::ensureOperatorExists() } } -void Session::addedToHistory(protocol::MessagePtr msg) +void Session::addedToHistory(const net::Message &msg) { - if(m_recorder) - m_recorder->recordMessage(msg); - + if(m_recorder) { + if(!DP_recorder_message_push_inc(m_recorder, msg.get())) { + DP_recorder_free_join(m_recorder, nullptr); + m_recorder = nullptr; + } + } m_lastEventTime.start(); // TODO calculate activity score that can be shown in listings } @@ -863,72 +963,76 @@ void Session::addedToHistory(protocol::MessagePtr msg) void Session::restartRecording() { if(m_recorder) { - m_recorder->close(); - delete m_recorder; + DP_recorder_free_join(m_recorder, nullptr); } // Start recording QString filename = utils::makeFilenameUnique(m_recordingFile, ".dprec"); qDebug("Starting session recording %s", qPrintable(filename)); - m_recorder = new recording::Writer(filename, this); - if(!m_recorder->open()) { - qWarning("Couldn't write session recording to %s: %s", qPrintable(filename), qPrintable(m_recorder->errorString())); - delete m_recorder; - m_recorder = nullptr; + DP_Output *output = DP_file_output_new_from_path(qUtf8Printable(filename)); + m_recorder = + output + ? DP_recorder_new_inc( + DP_RECORDER_TYPE_BINARY, + DP_recorder_header_new( + "server-recording", "true", "version", + qUtf8Printable(m_history->protocolVersion().asString()), + static_cast(nullptr)), + nullptr, nullptr, nullptr, output) + : nullptr; + if(!m_recorder) { + qWarning( + "Couldn't write session recording to %s: %s", + qUtf8Printable(filename), DP_error()); return; } - QJsonObject metadata; - metadata["server-recording"] = true; - metadata["version"] = m_history->protocolVersion().asString(); - - m_recorder->writeHeader(metadata); - m_recorder->setAutoflush(); - - int lastBatchIndex=0; + int lastBatchIndex = 0; do { - protocol::MessageList history; + net::MessageList history; std::tie(history, lastBatchIndex) = m_history->getBatch(lastBatchIndex); - for(MessagePtr m : history) - m_recorder->recordMessage(m); + for(const net::Message &msg : history) { + DP_recorder_message_push_inc(m_recorder, msg.get()); + } - } while(lastBatchIndexlastIndex()); + } while(lastBatchIndex < m_history->lastIndex()); } void Session::stopRecording() { if(m_recorder) { - m_recorder->close(); - delete m_recorder; + DP_recorder_free_join(m_recorder, nullptr); m_recorder = nullptr; } } QString Session::uptime() const { - qint64 up = (QDateTime::currentMSecsSinceEpoch() - m_history->startTime().toMSecsSinceEpoch()) / 1000; + qint64 up = (QDateTime::currentMSecsSinceEpoch() - + m_history->startTime().toMSecsSinceEpoch()) / + 1000; - int days = up / (60*60*24); - up -= days * (60*60*24); + int days = up / (60 * 60 * 24); + up -= days * (60 * 60 * 24); - int hours = up / (60*60); - up -= hours * (60*60); + int hours = up / (60 * 60); + up -= hours * (60 * 60); int minutes = up / 60; QString uptime; - if(days==1) + if(days == 1) uptime = "one day, "; - else if(days>1) + else if(days > 1) uptime = QString::number(days) + " days, "; - if(hours==1) + if(hours == 1) uptime += "1 hour and "; else uptime += QString::number(hours) + " hours and "; - if(minutes==1) + if(minutes == 1) uptime += "1 minute"; else uptime += QString::number(minutes) + " minutes."; @@ -947,7 +1051,10 @@ QStringList Session::userNames() const void Session::makeAnnouncement(const QUrl &url, bool privateListing) { Q_ASSERT(m_announcements); - m_announcements->announceSession(this, url, privateListing ? sessionlisting::PrivacyMode::Private : sessionlisting::PrivacyMode::Public); + m_announcements->announceSession( + this, url, + privateListing ? sessionlisting::PrivacyMode::Private + : sessionlisting::PrivacyMode::Public); } void Session::unlistAnnouncement(const QUrl &url, bool terminate) @@ -961,16 +1068,19 @@ void Session::unlistAnnouncement(const QUrl &url, bool terminate) sessionlisting::Session Session::getSessionAnnouncement() const { - const bool privateUserList = m_config->getConfigBool(config::PrivateUserList); + const bool privateUserList = + m_config->getConfigBool(config::PrivateUserList); - return sessionlisting::Session { + return sessionlisting::Session{ m_config->internalConfig().localHostname, m_config->internalConfig().getAnnouncePort(), aliasOrId(), m_history->protocolVersion(), m_history->title(), userCount(), - (!m_history->passwordHash().isEmpty() || privateUserList) ? QStringList() : userNames(), + (!m_history->passwordHash().isEmpty() || privateUserList) + ? QStringList() + : userNames(), !m_history->passwordHash().isEmpty(), m_history->hasFlag(SessionHistory::Nsfm), sessionlisting::PrivacyMode::Undefined, @@ -981,13 +1091,15 @@ sessionlisting::Session Session::getSessionAnnouncement() const }; } -bool Session::hasUrgentAnnouncementChange(const sessionlisting::Session &description) const +bool Session::hasUrgentAnnouncementChange( + const sessionlisting::Session &description) const { return description.title != m_history->title() || - description.nsfm != m_history->hasFlag(SessionHistory::Nsfm) || - description.password != !m_history->passwordHash().isEmpty() || - (description.users >= description.maxUsers) != (userCount() >= m_history->maxUsers()) || - description.closed != m_closed; + description.nsfm != m_history->hasFlag(SessionHistory::Nsfm) || + description.password != !m_history->passwordHash().isEmpty() || + (description.users >= description.maxUsers) != + (userCount() >= m_history->maxUsers()) || + description.closed != m_closed; } @@ -997,7 +1109,8 @@ void Session::onAnnouncementsChanged(const sessionlisting::Announcable *session) sendUpdatedAnnouncementList(); } -void Session::onAnnouncementError(const Announcable *session, const QString &message) +void Session::onAnnouncementError( + const Announcable *session, const QString &message) { if(session == this) { messageAll(message, false); @@ -1008,29 +1121,43 @@ void Session::onConfigValueChanged(const ConfigKey &key) { if(key.index == config::ForceNsfm.index) { if(forceEnableNsfm(m_history, m_config)) { - log(Log().about(Log::Level::Info, Log::Topic::Status).message("Forced NSFM after config change")); + log(Log() + .about(Log::Level::Info, Log::Topic::Status) + .message("Forced NSFM after config change")); } sendUpdatedSessionProperties(); } } -void Session::sendAbuseReport(const Client *reporter, int aboutUser, const QString &message) +void Session::sendAbuseReport( + const Client *reporter, int aboutUser, const QString &message) { Q_ASSERT(reporter); - reporter->log(Log().about(Log::Level::Info, Log::Topic::Status).message(QString("Abuse report about user %1 received: %2").arg(aboutUser).arg(message))); + reporter->log( + Log() + .about(Log::Level::Info, Log::Topic::Status) + .message(QString("Abuse report about user %1 received: %2") + .arg(aboutUser) + .arg(message))); const QUrl url = m_config->internalConfig().reportUrl; if(!url.isValid()) { // This shouldn't happen normally. If the URL is not configured, // the server does not advertise the capability to receive reports. - log(Log().about(Log::Level::Warn, Log::Topic::Status).message("Cannot send abuse report: server URL not configured!")); + log(Log() + .about(Log::Level::Warn, Log::Topic::Status) + .message( + "Cannot send abuse report: server URL not configured!")); return; } if(!m_config->getConfigBool(config::AbuseReport)) { - // This can happen if reporting is disabled when a session is still in progress - log(Log().about(Log::Level::Warn, Log::Topic::Status).message("Cannot send abuse report: not enabled!")); + // This can happen if reporting is disabled when a session is still in + // progress + log(Log() + .about(Log::Level::Warn, Log::Topic::Status) + .message("Cannot send abuse report: not enabled!")); return; } @@ -1040,7 +1167,7 @@ void Session::sendAbuseReport(const Client *reporter, int aboutUser, const QStri o["user"] = reporter->username(); o["auth"] = reporter->isAuthenticated(); o["ip"] = reporter->peerAddress().toString(); - if(aboutUser>0) + if(aboutUser > 0) o["perp"] = aboutUser; o["message"] = message; @@ -1063,10 +1190,15 @@ void Session::sendAbuseReport(const Client *reporter, int aboutUser, const QStri req.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); if(!authToken.isEmpty()) req.setRawHeader("Authorization", "Token " + authToken.toUtf8()); - QNetworkReply *reply = networkaccess::getInstance()->post(req, QJsonDocument(o).toJson()); + QNetworkReply *reply = + networkaccess::getInstance()->post(req, QJsonDocument(o).toJson()); connect(reply, &QNetworkReply::finished, this, [this, reply]() { if(reply->error() != QNetworkReply::NoError) { - log(Log().about(Log::Level::Warn, Log::Topic::Status).message("Unable to send abuse report: " + reply->errorString())); + log(Log() + .about(Log::Level::Warn, Log::Topic::Status) + .message( + "Unable to send abuse report: " + + reply->errorString())); } }); connect(reply, &QNetworkReply::finished, reply, &QObject::deleteLater); @@ -1076,7 +1208,7 @@ QJsonObject Session::getDescription(bool full) const { // The basic description contains just the information // needed for the login session listing - QJsonObject o { + QJsonObject o{ {"id", id()}, {"alias", idAlias()}, {"protocol", m_history->protocolVersion().asString()}, @@ -1089,8 +1221,7 @@ QJsonObject Session::getDescription(bool full) const {"authOnly", m_history->hasFlag(SessionHistory::AuthOnly)}, {"nsfm", m_history->hasFlag(SessionHistory::Nsfm)}, {"startTime", m_history->startTime().toString(Qt::ISODate)}, - {"size", int(m_history->sizeInBytes())} - }; + {"size", int(m_history->sizeInBytes())}}; if(m_config->getConfigBool(config::EnablePersistence)) o["persistent"] = m_history->hasFlag(SessionHistory::Persistent); @@ -1108,25 +1239,24 @@ QJsonObject Session::getDescription(bool full) const u["online"] = true; users << u; } - for(auto u=m_pastClients.constBegin();u!=m_pastClients.constEnd();++u) { - users << QJsonObject { + for(auto u = m_pastClients.constBegin(); u != m_pastClients.constEnd(); + ++u) { + users << QJsonObject{ {"id", u->id}, {"name", u->username}, {"ip", u->peerAddress.toString()}, - {"online", false} - }; + {"online", false}}; } o["users"] = users; QJsonArray listings; const auto announcements = m_announcements->getAnnouncements(this); for(const sessionlisting::Announcement &a : announcements) { - listings << QJsonObject { + listings << QJsonObject{ {"id", a.listingId}, {"url", a.apiUrl.toString()}, {"roomcode", a.roomcode}, - {"private", a.isPrivate} - }; + {"private", a.isPrivate}}; } o["listings"] = listings; } @@ -1134,7 +1264,8 @@ QJsonObject Session::getDescription(bool full) const return o; } -JsonApiResult Session::callJsonApi(JsonApiMethod method, const QStringList &path, const QJsonObject &request) +JsonApiResult Session::callJsonApi( + JsonApiMethod method, const QStringList &path, const QJsonObject &request) { if(!path.isEmpty()) { QString head; @@ -1145,7 +1276,7 @@ JsonApiResult Session::callJsonApi(JsonApiMethod method, const QStringList &path return callListingsJsonApi(method, tail, request); int userId = head.toInt(); - if(userId>0) { + if(userId > 0) { Client *c = getClientById(userId); if(c) return c->callJsonApi(method, tail, request); @@ -1164,13 +1295,16 @@ JsonApiResult Session::callJsonApi(JsonApiMethod method, const QStringList &path } else if(method == JsonApiMethod::Delete) { killSession(); - return JsonApiResult{ JsonApiResult::Ok, QJsonDocument(QJsonObject{ { "status", "ok "} }) }; + return JsonApiResult{ + JsonApiResult::Ok, QJsonDocument(QJsonObject{{"status", "ok "}})}; } - return JsonApiResult{JsonApiResult::Ok, QJsonDocument(getDescription(true))}; + return JsonApiResult{ + JsonApiResult::Ok, QJsonDocument(getDescription(true))}; } -JsonApiResult Session::callListingsJsonApi(JsonApiMethod method, const QStringList &path, const QJsonObject &request) +JsonApiResult Session::callListingsJsonApi( + JsonApiMethod method, const QStringList &path, const QJsonObject &request) { Q_UNUSED(request); if(path.length() != 1) @@ -1182,7 +1316,9 @@ JsonApiResult Session::callListingsJsonApi(JsonApiMethod method, const QStringLi if(a.listingId == id) { if(method == JsonApiMethod::Delete) { unlistAnnouncement(a.apiUrl.toString()); - return JsonApiResult{ JsonApiResult::Ok, QJsonDocument(QJsonObject{ { "status", "ok "} }) }; + return JsonApiResult{ + JsonApiResult::Ok, + QJsonDocument(QJsonObject{{"status", "ok "}})}; } else { return JsonApiBadMethod(); @@ -1204,4 +1340,3 @@ void Session::log(const Log &log) } } - diff --git a/src/libserver/session.h b/src/libserver/session.h index 988b4796b0..a325fccb5a 100644 --- a/src/libserver/session.h +++ b/src/libserver/session.h @@ -1,29 +1,23 @@ // SPDX-License-Identifier: GPL-3.0-or-later - -#ifndef DP_SHARED_SERVER_SESSION_H -#define DP_SHARED_SERVER_SESSION_H - +#ifndef LIBSHARED_SERVER_SESSION_H +#define LIBSHARED_SERVER_SESSION_H #include "libserver/announcable.h" +#include "libserver/jsonapi.h" +#include "libserver/sessionhistory.h" #include "libshared/net/message.h" #include "libshared/net/protover.h" -#include "libserver/sessionhistory.h" -#include "libserver/jsonapi.h" - -#include -#include -#include #include #include +#include #include +#include +#include class QTimer; - -namespace recording { - class Writer; -} +struct DP_Recorder; namespace sessionlisting { - class Announcements; +class Announcements; } namespace server { @@ -36,19 +30,14 @@ class Log; /** * The serverside session state. * - * This is an abstract base class. Concrete implementations are ThinSession and ThickSession, - * for thin and thick servers respectively. + * This is an abstract base class. Concrete implementations are ThinSession and + * ThickSession, for thin and thick servers respectively. */ class Session : public QObject, public sessionlisting::Announcable { Q_OBJECT public: //! State of the session - enum class State { - Initialization, - Running, - Reset, - Shutdown - }; + enum class State { Initialization, Running, Reset, Shutdown }; //! Information about a user who has since logged out struct PastClient { @@ -68,12 +57,14 @@ class Session : public QObject, public sessionlisting::Announcable { /** * @brief Get the custom alias for the session ID * - * Session ID alias is optional. If set, it can be used in place of the ID when joining a session. + * Session ID alias is optional. If set, it can be used in place of the ID + * when joining a session. */ QString idAlias() const { return m_history->idAlias(); } //! Get the session alias if set, or the ID if not - QString aliasOrId() const { + QString aliasOrId() const + { return m_history->idAlias().isEmpty() ? id() : m_history->idAlias(); } @@ -83,7 +74,10 @@ class Session : public QObject, public sessionlisting::Announcable { * The recording will be created after a snapshot point has been created. * @param filename path to output file */ - void setRecordingFile(const QString &filename) { m_recordingFile = filename; } + void setRecordingFile(const QString &filename) + { + m_recordingFile = filename; + } /** * @brief Is the session closed to new users? @@ -131,10 +125,14 @@ class Session : public QObject, public sessionlisting::Announcable { Client *getClientById(uint8_t id); //! Has a client with the given ID been logged in (not currently)? - bool hasPastClientWithId(uint8_t id) const { return m_pastClients.contains(id); } + bool hasPastClientWithId(uint8_t id) const + { + return m_pastClients.contains(id); + } //! Get information about a past client who used the given ID - PastClient getPastClientById(uint8_t id) const { + PastClient getPastClientById(uint8_t id) const + { Q_ASSERT(hasPastClientWithId(id)); return m_pastClients[id]; } @@ -165,7 +163,7 @@ class Session : public QObject, public sessionlisting::Announcable { int userCount() const { return m_clients.size(); } //! Get the of clients currently in this session - const QList &clients() const { return m_clients; } + const QVector &clients() const { return m_clients; } /** * @brief Get the ID of the user uploading initialization or reset data @@ -197,7 +195,7 @@ class Session : public QObject, public sessionlisting::Announcable { * @param client * @param message */ - void handleClientMessage(Client &client, protocol::MessagePtr message); + void handleClientMessage(Client &client, const net::Message &message); /** * @brief Initiate the shutdown of this session @@ -206,7 +204,7 @@ class Session : public QObject, public sessionlisting::Announcable { * will not be terminated. This allows the session to survive * server restarts. */ - void killSession(bool terminate=true); + void killSession(bool terminate = true); /** * @brief Send a direct message to all session participants @@ -214,7 +212,7 @@ class Session : public QObject, public sessionlisting::Announcable { * This bypasses the session history. * @param msg */ - void directToAll(protocol::MessagePtr msg); + void directToAll(const net::Message &msg); /** * @brief Send a message to every user of this session @@ -234,16 +232,22 @@ class Session : public QObject, public sessionlisting::Announcable { /** * @brief Generate a request for session announcement unlisting * @param url API url - * @param terminate if false, the removal is not logged in the history journal - * @param removeOnly if true, an unlisting request is not sent (use in case of error) + * @param terminate if false, the removal is not logged in the history + * journal + * @param removeOnly if true, an unlisting request is not sent (use in case + * of error) */ - void unlistAnnouncement(const QUrl &url, bool terminate=true); + void unlistAnnouncement(const QUrl &url, bool terminate = true); sessionlisting::Session getSessionAnnouncement() const override; - bool hasUrgentAnnouncementChange(const sessionlisting::Session &description) const override; + bool hasUrgentAnnouncementChange( + const sessionlisting::Session &description) const override; - void sendListserverMessage(const QString &message) override { messageAll(message, false); } + void sendListserverMessage(const QString &message) override + { + messageAll(message, false); + } //! Get the session state State state() const { return m_state; } @@ -269,7 +273,8 @@ class Session : public QObject, public sessionlisting::Announcable { * @param trusted new status * @param changedBy name of the user who issued the command */ - void changeTrustedStatus(uint8_t id, bool trusted, const QString &changedBy); + void + changeTrustedStatus(uint8_t id, bool trusted, const QString &changedBy); //! Send refreshed ban list to all logged in users void sendUpdatedBanlist(); @@ -286,10 +291,12 @@ class Session : public QObject, public sessionlisting::Announcable { * A reporting backend server must have been configured. * * @param reporter the user who is making the report - * @param aboutUser the ID of the user this report is about (if 0, the report is about the session in general) + * @param aboutUser the ID of the user this report is about (if 0, the + * report is about the session in general) * @param message freeform message entered by the reporter */ - void sendAbuseReport(const Client *reporter, int aboutUser, const QString &message); + void sendAbuseReport( + const Client *reporter, int aboutUser, const QString &message); /** * @brief Get a JSON object describing the session @@ -300,7 +307,7 @@ class Session : public QObject, public sessionlisting::Announcable { * @param full - include detailed information (for admin use) * @return */ - QJsonObject getDescription(bool full=false) const; + QJsonObject getDescription(bool full = false) const; /** * @brief Call the server's JSON administration API @@ -312,7 +319,9 @@ class Session : public QObject, public sessionlisting::Announcable { * @param request request body content * @return JSON API response content */ - JsonApiResult callJsonApi(JsonApiMethod method, const QStringList &path, const QJsonObject &request); + JsonApiResult callJsonApi( + JsonApiMethod method, const QStringList &path, + const QJsonObject &request); /** * @brief Write a session related log entry. @@ -340,14 +349,17 @@ class Session : public QObject, public sessionlisting::Announcable { private slots: void removeUser(Client *user); void onAnnouncementsChanged(const Announcable *session); - void onAnnouncementError(const Announcable *session, const QString &message); + void + onAnnouncementError(const Announcable *session, const QString &message); void onConfigValueChanged(const ConfigKey &key); protected: - Session(SessionHistory *history, ServerConfig *config, sessionlisting::Announcements *announcements, QObject *parent); + Session( + SessionHistory *history, ServerConfig *config, + sessionlisting::Announcements *announcements, QObject *parent); //! Add a message to the session history - virtual void addToHistory(protocol::MessagePtr msg) = 0; + virtual void addToHistory(const net::Message &msg) = 0; //! Session history was just reset virtual void onSessionReset() = 0; @@ -356,14 +368,17 @@ private slots: virtual void onClientJoin(Client *client, bool host) = 0; //! This message was just added to session history - void addedToHistory(protocol::MessagePtr msg); + void addedToHistory(const net::Message &msg); void switchState(State newstate); - //! Get the user join, SessionOwner, etc. messages that should be prepended to a reset image - protocol::MessageList serverSideStateMessages() const; + //! Get the user join, SessionOwner, etc. messages that should be prepended + //! to a reset image + net::MessageList serverSideStateMessages() const; private: + void addClientMessage(const Client &client, const net::Message &msg); + /** * Add a message to the initialization stream * @@ -371,7 +386,7 @@ private slots: * During reset state, this goes to a reset buffer which will * replace the old session history once completed. */ - void addToInitStream(protocol::MessagePtr msg); + void addToInitStream(const net::Message &msg); /** * @brief Update session operator bits @@ -382,7 +397,8 @@ private slots: * @param changedBy name of the user who issued the change command * @return sanitized list of actual session operators */ - QList updateOwnership(QList ids, const QString &changedBy); + QVector + updateOwnership(QVector ids, const QString &changedBy); /** * @brief Update the list of trusted users @@ -392,7 +408,8 @@ private slots: * @param changedBy name of the user who issued the change command * @return sanitized list of actual trusted users */ - QList updateTrustedUsers(QList ids, const QString &changedBy); + QVector + updateTrustedUsers(QVector ids, const QString &changedBy); void restartRecording(); void stopRecording(); @@ -401,7 +418,9 @@ private slots: void sendUpdatedSessionProperties(); void ensureOperatorExists(); - JsonApiResult callListingsJsonApi(JsonApiMethod method, const QStringList &path, const QJsonObject &request); + JsonApiResult callListingsJsonApi( + JsonApiMethod method, const QStringList &path, + const QJsonObject &request); SessionHistory *m_history; ServerConfig *m_config; @@ -410,13 +429,13 @@ private slots: State m_state = State::Initialization; int m_initUser = -1; // the user who is currently uploading init/reset data - recording::Writer *m_recorder = nullptr; + DP_Recorder *m_recorder = nullptr; QString m_recordingFile; - QList m_clients; + QVector m_clients; QHash m_pastClients; - protocol::MessageList m_resetstream; + net::MessageList m_resetstream; uint m_resetstreamsize = 0; QElapsedTimer m_lastEventTime; @@ -426,12 +445,9 @@ private slots: // https://gcc.gnu.org/bugzilla/show_bug.cgi?id=69210 namespace diagnostic_marker_private { - class [[maybe_unused]] AbstractSessionMarker : Session - { - }; +class [[maybe_unused]] AbstractSessionMarker : Session {}; } } #endif - diff --git a/src/libserver/sessionhistory.cpp b/src/libserver/sessionhistory.cpp index 9830bf1df0..f35b7996db 100644 --- a/src/libserver/sessionhistory.cpp +++ b/src/libserver/sessionhistory.cpp @@ -1,20 +1,26 @@ // SPDX-License-Identifier: GPL-3.0-or-later - #include "libserver/sessionhistory.h" namespace server { SessionHistory::SessionHistory(const QString &id, QObject *parent) - : QObject(parent), m_id(id), m_startTime(QDateTime::currentDateTimeUtc()), - m_sizeInBytes(0), m_sizeLimit(0), m_autoResetBaseSize(0), - m_firstIndex(0), m_lastIndex(-1) + : QObject(parent) + , m_id(id) + , m_startTime(QDateTime::currentDateTimeUtc()) + , m_sizeInBytes(0) + , m_sizeLimit(0) + , m_autoResetBaseSize(0) + , m_firstIndex(0) + , m_lastIndex(-1) { } -bool SessionHistory::addBan(const QString &username, const QHostAddress &ip, const QString &extAuthId, const QString &bannedBy) +bool SessionHistory::addBan( + const QString &username, const QHostAddress &ip, const QString &extAuthId, + const QString &bannedBy) { const int id = m_banlist.addBan(username, ip, extAuthId, bannedBy); - if(id>0) { + if(id > 0) { historyAddBan(id, username, ip, extAuthId, bannedBy); return true; } @@ -36,32 +42,33 @@ void SessionHistory::joinUser(uint8_t id, const QString &name) void SessionHistory::historyLoaded(uint size, int messageCount) { - Q_ASSERT(m_lastIndex==-1); + Q_ASSERT(m_lastIndex == -1); m_sizeInBytes = size; m_lastIndex = messageCount - 1; m_autoResetBaseSize = size; } -bool SessionHistory::addMessage(const protocol::MessagePtr &msg) +bool SessionHistory::addMessage(const net::Message &msg) { if(isOutOfSpace()) return false; - m_sizeInBytes += msg->length(); + m_sizeInBytes += msg.length(); ++m_lastIndex; historyAdd(msg); emit newMessagesAvailable(); return true; } -bool SessionHistory::reset(const protocol::MessageList &newHistory) +bool SessionHistory::reset(const net::MessageList &newHistory) { uint newSize = 0; - for(const protocol::MessagePtr &msg : newHistory) { - newSize += msg->length(); + for(const net::Message &msg : newHistory) { + newSize += msg.length(); } - if(m_sizeLimit>0 && newSize > m_sizeLimit) + if(m_sizeLimit > 0 && newSize > m_sizeLimit) { return false; + } m_sizeInBytes = newSize; m_firstIndex = m_lastIndex + 1; @@ -76,13 +83,13 @@ uint SessionHistory::effectiveAutoResetThreshold() const { uint t = autoResetThreshold(); // Zero means autoreset is not enabled - if(t>0) { + if(t > 0) { t += m_autoResetBaseSize; - if(m_sizeLimit>0) + if(m_sizeLimit > 0) { t = qMin(t, uint(m_sizeLimit * 0.9)); + } } return t; } } - diff --git a/src/libserver/sessionhistory.h b/src/libserver/sessionhistory.h index 6c95b81569..ebe844a131 100644 --- a/src/libserver/sessionhistory.h +++ b/src/libserver/sessionhistory.h @@ -1,19 +1,16 @@ // SPDX-License-Identifier: GPL-3.0-or-later - -#ifndef DP_SERVER_SESSION_HISTORY_H -#define DP_SERVER_SESSION_HISTORY_H - +#ifndef LIBSERVER_SESSION_HISTORY_H +#define LIBSERVER_SESSION_HISTORY_H +#include "libserver/idqueue.h" +#include "libserver/sessionban.h" #include "libshared/net/message.h" #include "libshared/util/passwordhash.h" -#include "libserver/sessionban.h" -#include "libserver/idqueue.h" - -#include #include +#include #include namespace protocol { - class ProtocolVersion; +class ProtocolVersion; } namespace server { @@ -60,18 +57,27 @@ class SessionHistory : public QObject { * An empty QByteArray is returned if no password is set */ virtual QByteArray passwordHash() const = 0; - bool checkPassword(const QString &password) { return passwordhash::check(password, passwordHash()); } + bool checkPassword(const QString &password) + { + return passwordhash::check(password, passwordHash()); + } //! Set (or clear) this session's password virtual void setPasswordHash(const QByteArray &passwordHash) = 0; - void setPassword(const QString &password) { setPasswordHash(passwordhash::hash(password)); } + void setPassword(const QString &password) + { + setPasswordHash(passwordhash::hash(password)); + } //! Get the operator password hash virtual QByteArray opwordHash() const = 0; //! Set (or clear) the operator password virtual void setOpwordHash(const QByteArray &opword) = 0; - void setOpword(const QString &opword) { setOpwordHash(passwordhash::hash(opword)); } + void setOpword(const QString &opword) + { + setOpwordHash(passwordhash::hash(opword)); + } //! Get the starting timestamp QDateTime startTime() const { return m_startTime; } @@ -104,7 +110,8 @@ class SessionHistory : public QObject { //! Get the history autoreset request threshold virtual uint autoResetThreshold() const = 0; - //! Get the final autoreset threshold that includes the reset image base size + //! Get the final autoreset threshold that includes the reset image base + //! size uint effectiveAutoResetThreshold() const; //! Get the reset image base size @@ -117,7 +124,7 @@ class SessionHistory : public QObject { * * @return false if there was no space for this message */ - bool addMessage(const protocol::MessagePtr &msg); + bool addMessage(const net::Message &msg); /** * @brief Reset the session history @@ -129,7 +136,7 @@ class SessionHistory : public QObject { * * @return false if new history is larger than the size limit */ - bool reset(const protocol::MessageList &newHistory); + bool reset(const net::MessageList &newHistory); /** * @brief Get a batch of messages @@ -141,7 +148,7 @@ class SessionHistory : public QObject { * The second element of the tuple is the index of the last message * in the batch, or lastIndex() if there were no more available messages */ - virtual std::tuple getBatch(int after) const = 0; + virtual std::tuple getBatch(int after) const = 0; /** * @brief Mark messages before the given index as unneeded (for now) @@ -185,7 +192,10 @@ class SessionHistory : public QObject { /** * @brief Has the session ran out of space */ - bool isOutOfSpace() const { return m_sizeLimit>0 && m_sizeInBytes >= m_sizeLimit; } + bool isOutOfSpace() const + { + return m_sizeLimit > 0 && m_sizeInBytes >= m_sizeLimit; + } /** * @brief Get the index number of the first message in history @@ -208,7 +218,9 @@ class SessionHistory : public QObject { /** * @brief Add a new banlist entry */ - bool addBan(const QString &username, const QHostAddress &ip, const QString &extAuthId, const QString &bannedBy); + bool addBan( + const QString &username, const QHostAddress &ip, + const QString &extAuthId, const QString &bannedBy); /** * @brief removeBan Remove a banlist entry @@ -276,15 +288,17 @@ class SessionHistory : public QObject { /** * @brief This signal is emited when new messages are added to the history * - * Clients whose upload queues are empty should get the new message batch and - * start sending. + * Clients whose upload queues are empty should get the new message batch + * and start sending. */ void newMessagesAvailable(); protected: - virtual void historyAdd(const protocol::MessagePtr &msg) = 0; - virtual void historyReset(const protocol::MessageList &newHistory) = 0; - virtual void historyAddBan(int id, const QString &username, const QHostAddress &ip, const QString &extAuthId, const QString &bannedBy) = 0; + virtual void historyAdd(const net::Message &msg) = 0; + virtual void historyReset(const net::MessageList &newHistory) = 0; + virtual void historyAddBan( + int id, const QString &username, const QHostAddress &ip, + const QString &extAuthId, const QString &bannedBy) = 0; virtual void historyRemoveBan(int id) = 0; void historyLoaded(uint size, int messageCount); @@ -304,9 +318,9 @@ class SessionHistory : public QObject { // https://gcc.gnu.org/bugzilla/show_bug.cgi?id=69210 namespace diagnostic_marker_private { - class [[maybe_unused]] AbstractSessionHistoryMarker : SessionHistory { - inline void joinUser(uint8_t, const QString&) override {} - }; +class [[maybe_unused]] AbstractSessionHistoryMarker : SessionHistory { + inline void joinUser(uint8_t, const QString &) override {} +}; } Q_DECLARE_OPERATORS_FOR_FLAGS(SessionHistory::Flags) @@ -314,4 +328,3 @@ Q_DECLARE_OPERATORS_FOR_FLAGS(SessionHistory::Flags) } #endif - diff --git a/src/libserver/sessionserver.cpp b/src/libserver/sessionserver.cpp index 3a59084c15..1c4ebaeab0 100644 --- a/src/libserver/sessionserver.cpp +++ b/src/libserver/sessionserver.cpp @@ -29,10 +29,6 @@ SessionServer::SessionServer(ServerConfig *config, QObject *parent) connect(cleanupTimer, &QTimer::timeout, this, &SessionServer::cleanupSessions); cleanupTimer->setInterval(15 * 1000); cleanupTimer->start(cleanupTimer->interval()); - -#ifndef NDEBUG - m_randomlag = 0; -#endif } void SessionServer::setSessionDir(const QDir &dir) @@ -213,10 +209,6 @@ void SessionServer::addClient(ThinServerClient *client) client->setParent(this); client->setConnectionTimeout(m_config->getConfigTime(config::ClientTimeout) * 1000); -#ifndef NDEBUG - client->setRandomLag(m_randomlag); -#endif - m_clients.append(client); connect(client, &Client::destroyed, this, &SessionServer::removeClient); diff --git a/src/libserver/sessionserver.h b/src/libserver/sessionserver.h index 5a68a30c7e..dce54af75b 100644 --- a/src/libserver/sessionserver.h +++ b/src/libserver/sessionserver.h @@ -53,10 +53,6 @@ Q_OBJECT */ const ServerConfig *config() const { return m_config; } -#ifndef NDEBUG - void setRandomLag(uint lag) { m_randomlag = lag; } -#endif - /** * @brief Add a new client * @@ -183,10 +179,6 @@ private slots: QList m_sessions; QList m_clients; - -#ifndef NDEBUG - uint m_randomlag; -#endif }; } diff --git a/src/libserver/tests/filedhistory.cpp b/src/libserver/tests/filedhistory.cpp index 85c933bdc0..5a3610217c 100644 --- a/src/libserver/tests/filedhistory.cpp +++ b/src/libserver/tests/filedhistory.cpp @@ -1,19 +1,15 @@ // SPDX-License-Identifier: GPL-3.0-or-later - #include "libserver/filedhistory.h" #include "libshared/util/passwordhash.h" #include "libshared/util/ulid.h" -#include "libshared/net/meta.h" - -#include -#include #include +#include +#include #include using namespace server; -class TestFiledHistory final : public QObject -{ +class TestFiledHistory final : public QObject { Q_OBJECT private slots: void initTestCase() @@ -30,9 +26,12 @@ private slots: const int maxUsers = 11; const QString title = "Hello world"; const QString idAlias = "test"; - const protocol::ProtocolVersion protover = protocol::ProtocolVersion::current(); + const protocol::ProtocolVersion protover = + protocol::ProtocolVersion::current(); const QString founder = "me!"; - const SessionHistory::Flags flags = SessionHistory::Persistent | SessionHistory::PreserveChat | SessionHistory::Nsfm; + const SessionHistory::Flags flags = SessionHistory::Persistent | + SessionHistory::PreserveChat | + SessionHistory::Nsfm; const QString bannedUser = "troll user with a long \\ \"name}%["; const QString opUser = "op"; @@ -43,17 +42,22 @@ private slots: auto testId = Ulid::make().toString(); { - std::unique_ptr fh { FiledHistory::startNew(m_dir, testId, idAlias, protover, founder) }; + std::unique_ptr fh{FiledHistory::startNew( + m_dir, testId, idAlias, protover, founder)}; QVERIFY(fh.get()); fh->setPasswordHash(passwordhash::hash(password)); fh->setOpwordHash(passwordhash::hash(opword)); fh->setMaxUsers(200); - fh->setMaxUsers(maxUsers); // this should replace the previously set value + fh->setMaxUsers( + maxUsers); // this should replace the previously set value fh->setTitle(title); fh->setFlags(flags); fh->addBan(bannedUser, bannedAddress, QString(), opUser); - fh->addBan("test", QHostAddress("192.168.0.101"), QString(), opUser); - fh->addBan("test3", QHostAddress("192.168.0.102"), bannedExtAuthId, opUser); + fh->addBan( + "test", QHostAddress("192.168.0.101"), QString(), opUser); + fh->addBan( + "test3", QHostAddress("192.168.0.102"), bannedExtAuthId, + opUser); fh->removeBan(2); fh->addAnnouncement(announcementUrl); fh->addAnnouncement("http://example.com/2/"); @@ -65,11 +69,13 @@ private slots: fh->setAuthenticatedOperator("u1", false); // The history file must have some content before it can be loaded - fh->addMessage(protocol::MessagePtr(new protocol::Chat(1, 0, 0, QByteArray("test")))); + fh->addMessage( + net::makeChatMessage(1, 0, 0, QStringLiteral("test"))); } { - std::unique_ptr fh { FiledHistory::load(m_dir.absoluteFilePath(FiledHistory::journalFilename(testId))) }; + std::unique_ptr fh{FiledHistory::load( + m_dir.absoluteFilePath(FiledHistory::journalFilename(testId)))}; QVERIFY(fh.get()); QCOMPARE(fh->id(), testId); @@ -84,13 +90,21 @@ private slots: QJsonArray banlist = fh->banlist().toJson(true); QCOMPARE(banlist.size(), 2); - QCOMPARE(banlist.at(0).toObject()["username"].toString(), bannedUser); + QCOMPARE( + banlist.at(0).toObject()["username"].toString(), bannedUser); QCOMPARE(banlist.at(0).toObject()["bannedBy"].toString(), opUser); - QCOMPARE(banlist.at(0).toObject()["ip"].toString(), bannedAddress.toString()); - QCOMPARE(banlist.at(0).toObject()["extauthid"].toString(), QString()); - - QCOMPARE(banlist.at(1).toObject()["username"].toString(), QString("test3")); - QCOMPARE(banlist.at(1).toObject()["extauthid"].toString(), bannedExtAuthId); + QCOMPARE( + banlist.at(0).toObject()["ip"].toString(), + bannedAddress.toString()); + QCOMPARE( + banlist.at(0).toObject()["extauthid"].toString(), QString()); + + QCOMPARE( + banlist.at(1).toObject()["username"].toString(), + QString("test3")); + QCOMPARE( + banlist.at(1).toObject()["extauthid"].toString(), + bannedExtAuthId); QCOMPARE(banlist.at(1).toObject()["bannedBy"].toString(), opUser); QStringList announcements = fh->announcements(); @@ -106,31 +120,39 @@ private slots: } } + static QString getChatMessage(const net::Message &msg) + { + size_t len; + const char *message = DP_msg_chat_message(msg.toChat(), &len); + return QString::fromUtf8(message, len); + } + // Test that a recording can be loaded correctly void testLoading() { QString file = makeTestRecording(); - std::unique_ptr fh { FiledHistory::load(m_dir.absoluteFilePath(file)) }; + std::unique_ptr fh{ + FiledHistory::load(m_dir.absoluteFilePath(file))}; - protocol::MessageList msgs; + net::MessageList msgs; int lastIdx; std::tie(msgs, lastIdx) = fh->getBatch(-1); QCOMPARE(msgs.size(), 3); - QCOMPARE(msgs.at(0).cast().message(), QString("test1")); + QCOMPARE(getChatMessage(msgs.at(0)), QString("test1")); QCOMPARE(lastIdx, 2); std::tie(msgs, lastIdx) = fh->getBatch(0); QCOMPARE(msgs.size(), 2); - QCOMPARE(msgs.at(0).cast().message(), QString("test2")); + QCOMPARE(getChatMessage(msgs.at(0)), QString("test2")); QCOMPARE(lastIdx, 2); std::tie(msgs, lastIdx) = fh->getBatch(1); QCOMPARE(msgs.size(), 1); - QCOMPARE(msgs.at(0).cast().message(), QString("test3")); + QCOMPARE(getChatMessage(msgs.at(0)), QString("test3")); QCOMPARE(lastIdx, 2); std::tie(msgs, lastIdx) = fh->getBatch(2); @@ -144,18 +166,21 @@ private slots: void testLoadAppend() { QString file = makeTestRecording(); - std::unique_ptr fh { FiledHistory::load(m_dir.absoluteFilePath(file)) }; + std::unique_ptr fh{ + FiledHistory::load(m_dir.absoluteFilePath(file))}; // Read the whole recording to load it into the cache fh->getBatch(-1); // Add something to it. This should go to the cache as well - auto testMsg = protocol::MessagePtr(new protocol::Chat(1, 0, 0, QByteArray("appended"))); + auto testMsg = + net::makeChatMessage(1, 0, 0, QStringLiteral("appended")); fh->addMessage(testMsg); - // The recording should now have a length of 4 and the last message should be there - protocol::MessageList msgs; + // The recording should now have a length of 4 and the last message + // should be there + net::MessageList msgs; int lastIdx; std::tie(msgs, lastIdx) = fh->getBatch(-1); @@ -176,16 +201,17 @@ private slots: QVERIFY(rf.resize(rf.size() - 3)); // The first two messages should still be readable - std::unique_ptr fh { FiledHistory::load(m_dir.absoluteFilePath(file)) }; + std::unique_ptr fh{ + FiledHistory::load(m_dir.absoluteFilePath(file))}; - protocol::MessageList msgs; + net::MessageList msgs; int lastIdx; std::tie(msgs, lastIdx) = fh->getBatch(-1); QCOMPARE(msgs.size(), 2); - QCOMPARE(msgs.at(0).cast().message(), QString("test1")); - QCOMPARE(msgs.at(1).cast().message(), QString("test2")); + QCOMPARE(getChatMessage(msgs.at(0)), QString("test1")); + QCOMPARE(getChatMessage(msgs.at(1)), QString("test2")); QCOMPARE(lastIdx, 1); } @@ -193,34 +219,36 @@ private slots: void testReset() { QString file = makeTestRecording(); - std::unique_ptr fh { FiledHistory::load(m_dir.absoluteFilePath(file)) }; + std::unique_ptr fh{ + FiledHistory::load(m_dir.absoluteFilePath(file))}; - auto testMsg = protocol::MessagePtr(new protocol::Chat(1, 0, 0, QByteArray("test0"))); + auto testMsg = net::makeChatMessage(1, 0, 0, QStringLiteral("test0")); fh->addMessage(testMsg); fh->addMessage(testMsg); fh->addMessage(testMsg); QCOMPARE(fh->lastIndex(), 5); - QCOMPARE(fh->sizeInBytes(), uint(testMsg->length()*6)); + QCOMPARE(fh->sizeInBytes(), uint(testMsg.length() * 6)); - protocol::MessageList msgs; + net::MessageList msgs; int lastIdx; std::tie(msgs, lastIdx) = fh->getBatch(-1); QCOMPARE(msgs.size(), 6); QCOMPARE(lastIdx, 5); QVERIFY(msgs.at(3).equals(testMsg)); - protocol::MessageList newContent; + net::MessageList newContent; newContent << testMsg; newContent << testMsg; fh->reset(newContent); QCOMPARE(fh->lastIndex(), 7); - QCOMPARE(fh->sizeInBytes(), uint(testMsg->length()*2)); + QCOMPARE(fh->sizeInBytes(), uint(testMsg.length() * 2)); - std::tie(msgs, lastIdx) = fh->getBatch(1); // any index below firstIndex() should work the same + // any index below firstIndex() should work the same + std::tie(msgs, lastIdx) = fh->getBatch(1); QCOMPARE(msgs.size(), 2); QCOMPARE(lastIdx, 7); QVERIFY(msgs.at(0).equals(testMsg)); @@ -229,13 +257,14 @@ private slots: void testBlockEnd() { QString file = makeTestRecording(); - std::unique_ptr fh { FiledHistory::load(m_dir.absoluteFilePath(file)) }; + std::unique_ptr fh{ + FiledHistory::load(m_dir.absoluteFilePath(file))}; QCOMPARE(fh->lastIndex(), 2); fh->closeBlock(); - auto testMsg = protocol::MessagePtr(new protocol::Chat(1, 0, 0, QByteArray("test0"))); + auto testMsg = net::makeChatMessage(1, 0, 0, QByteArray("test0")); fh->addMessage(testMsg); fh->addMessage(testMsg); @@ -243,18 +272,18 @@ private slots: QCOMPARE(fh->lastIndex(), 4); // First batch should contain the first block - protocol::MessageList msgs; + net::MessageList msgs; int lastIdx; std::tie(msgs, lastIdx) = fh->getBatch(-1); QCOMPARE(msgs.size(), 3); QCOMPARE(lastIdx, 2); - QCOMPARE(msgs.last().cast().message(), QString("test3")); + QCOMPARE(getChatMessage(msgs.last()), QString("test3")); // Second batch std::tie(msgs, lastIdx) = fh->getBatch(lastIdx); QCOMPARE(msgs.size(), 2); QCOMPARE(lastIdx, 4); - QCOMPARE(msgs.first().cast().message(), QString("test0")); + QCOMPARE(getChatMessage(msgs.first()), QString("test0")); // There is no third batch std::tie(msgs, lastIdx) = fh->getBatch(lastIdx); @@ -272,7 +301,7 @@ private slots: // Having an empty block at the end shouldn't have any effect fh->closeBlock(); - std::tie(msgs, lastIdx) = fh->getBatch(lastIdx-1); + std::tie(msgs, lastIdx) = fh->getBatch(lastIdx - 1); QCOMPARE(msgs.size(), 1); QCOMPARE(lastIdx, 5); } @@ -281,23 +310,29 @@ private slots: { auto id = Ulid::make().toString(); { - std::unique_ptr fh { FiledHistory::startNew(m_dir, id, QString(), protocol::ProtocolVersion::current(), "test") }; - - fh->addMessage(protocol::MessagePtr(new protocol::UserJoin(1, 0, QByteArray("u1"), QByteArray()))); - fh->addMessage(protocol::MessagePtr(new protocol::UserJoin(2, 0, QByteArray("u2"), QByteArray()))); - fh->addMessage(protocol::MessagePtr(new protocol::Chat(1, 0, 0, QByteArray("test1")))); - fh->addMessage(protocol::MessagePtr(new protocol::UserLeave(2))); + std::unique_ptr fh{FiledHistory::startNew( + m_dir, id, QString(), protocol::ProtocolVersion::current(), + "test")}; + + fh->addMessage( + net::makeJoinMessage(1, 0, QStringLiteral("u1"), QByteArray())); + fh->addMessage( + net::makeJoinMessage(2, 0, QStringLiteral("u2"), QByteArray())); + fh->addMessage( + net::makeChatMessage(1, 0, 0, QStringLiteral("test1"))); + fh->addMessage(net::makeLeaveMessage(2)); } { - std::unique_ptr fh { FiledHistory::load(m_dir.absoluteFilePath(FiledHistory::journalFilename(id))) }; + std::unique_ptr fh{FiledHistory::load( + m_dir.absoluteFilePath(FiledHistory::journalFilename(id)))}; QVERIFY(fh.get()); - protocol::MessageList msgs; + net::MessageList msgs; int lastIdx; std::tie(msgs, lastIdx) = fh->getBatch(-1); QCOMPARE(msgs.size(), 5); - QCOMPARE(msgs.last()->type(), protocol::MSG_USER_LEAVE); - QCOMPARE(msgs.last()->contextId(), uint8_t(1)); + QCOMPARE(msgs.last().type(), DP_MSG_LEAVE); + QCOMPARE(msgs.last().contextId(), uint8_t(1)); // Id Queue should have been updated by the leave events QVERIFY(fh->idQueue().nextId() > 2); @@ -309,11 +344,13 @@ private slots: QString makeTestRecording() { auto id = Ulid::make().toString(); - std::unique_ptr fh { FiledHistory::startNew(m_dir, id, QString(), protocol::ProtocolVersion::current(), "test") }; + std::unique_ptr fh{FiledHistory::startNew( + m_dir, id, QString(), protocol::ProtocolVersion::current(), + "test")}; - fh->addMessage(protocol::MessagePtr(new protocol::Chat(1, 0, 0, QByteArray("test1")))); - fh->addMessage(protocol::MessagePtr(new protocol::Chat(1, 0, 0, QByteArray("test2")))); - fh->addMessage(protocol::MessagePtr(new protocol::Chat(1, 0, 0, QByteArray("test3")))); + fh->addMessage(net::makeChatMessage(1, 0, 0, QStringLiteral("test1"))); + fh->addMessage(net::makeChatMessage(1, 0, 0, QStringLiteral("test2"))); + fh->addMessage(net::makeChatMessage(1, 0, 0, QStringLiteral("test3"))); return FiledHistory::journalFilename(id); } @@ -326,4 +363,3 @@ private slots: QTEST_MAIN(TestFiledHistory) #include "filedhistory.moc" - diff --git a/src/libserver/thinserverclient.cpp b/src/libserver/thinserverclient.cpp index 5a56dd16d6..0443a363a7 100644 --- a/src/libserver/thinserverclient.cpp +++ b/src/libserver/thinserverclient.cpp @@ -1,17 +1,18 @@ // SPDX-License-Identifier: GPL-3.0-or-later - #include "libserver/thinserverclient.h" -#include "libshared/net/messagequeue.h" #include "libserver/thinsession.h" +#include "libshared/net/messagequeue.h" namespace server { -ThinServerClient::ThinServerClient(QTcpSocket *socket, ServerLog *logger, QObject *parent) +ThinServerClient::ThinServerClient( + QTcpSocket *socket, ServerLog *logger, QObject *parent) : Client(socket, logger, parent) , m_historyPosition(-1) { - connect(messageQueue(), &protocol::MessageQueue::allSent, - this, &ThinServerClient::sendNextHistoryBatch); + connect( + messageQueue(), &net::MessageQueue::allSent, this, + &ThinServerClient::sendNextHistoryBatch); } void ThinServerClient::sendNextHistoryBatch() @@ -19,16 +20,18 @@ void ThinServerClient::sendNextHistoryBatch() // Only enqueue messages for uploading when upload queue is empty // and session is in a normal running state. // (We'll get another messagesAvailable signal when ready) - if(session() == nullptr || messageQueue()->isUploading() || session()->state() != Session::State::Running) + if(session() == nullptr || messageQueue()->isUploading() || + session()->state() != Session::State::Running) return; - protocol::MessageList batch; + net::MessageList batch; int batchLast; - std::tie(batch, batchLast) = session()->history()->getBatch(m_historyPosition); + std::tie(batch, batchLast) = + session()->history()->getBatch(m_historyPosition); m_historyPosition = batchLast; - messageQueue()->send(batch); + messageQueue()->sendMultiple(batch.size(), batch.constData()); - static_cast(session())->cleanupHistoryCache(); + static_cast(session())->cleanupHistoryCache(); } } diff --git a/src/libserver/thinserverclient.h b/src/libserver/thinserverclient.h index e7a0da65dc..9fd2b8b64f 100644 --- a/src/libserver/thinserverclient.h +++ b/src/libserver/thinserverclient.h @@ -1,16 +1,14 @@ // SPDX-License-Identifier: GPL-3.0-or-later - #ifndef THINSERVERCLIENT_H #define THINSERVERCLIENT_H - #include "libserver/client.h" namespace server { -class ThinServerClient final : public Client -{ +class ThinServerClient final : public Client { public: - ThinServerClient(QTcpSocket *socket, ServerLog *logger, QObject *parent=nullptr); + ThinServerClient( + QTcpSocket *socket, ServerLog *logger, QObject *parent = nullptr); /** * @brief Get this client's position in the session history diff --git a/src/libserver/thinsession.cpp b/src/libserver/thinsession.cpp index 2280c2e9bb..4f354cb30b 100644 --- a/src/libserver/thinsession.cpp +++ b/src/libserver/thinsession.cpp @@ -1,30 +1,34 @@ // SPDX-License-Identifier: GPL-3.0-or-later - #include "libserver/thinsession.h" -#include "libserver/thinserverclient.h" -#include "libserver/serverlog.h" #include "libserver/serverconfig.h" - -#include "libshared/net/control.h" +#include "libserver/serverlog.h" +#include "libserver/thinserverclient.h" +#include "libshared/net/message.h" +#include "libshared/net/servercmd.h" namespace server { -ThinSession::ThinSession(SessionHistory *history, ServerConfig *config, sessionlisting::Announcements *announcements, QObject *parent) +ThinSession::ThinSession( + SessionHistory *history, ServerConfig *config, + sessionlisting::Announcements *announcements, QObject *parent) : Session(history, config, announcements, parent) { history->setSizeLimit(config->getConfigSize(config::SessionSizeLimit)); - history->setAutoResetThreshold(config->getConfigSize(config::AutoresetThreshold)); + history->setAutoResetThreshold( + config->getConfigSize(config::AutoresetThreshold)); m_lastStatusUpdate.start(); } -void ThinSession::addToHistory(protocol::MessagePtr msg) +void ThinSession::addToHistory(const net::Message &msg) { if(state() == State::Shutdown) return; // Add message to history (if there is space) if(!history()->addMessage(msg)) { - messageAll("History size limit reached! Session must be reset to continue.", false); + messageAll( + "History size limit reached! Session must be reset to continue.", + false); return; } @@ -34,8 +38,9 @@ void ThinSession::addToHistory(protocol::MessagePtr msg) Client *origin = getClientById(initUserId()); Q_ASSERT(origin); if(origin) { - static_cast(origin)->setHistoryPosition(history()->lastIndex()); - if(!msg->isCommand()) + static_cast(origin)->setHistoryPosition( + history()->lastIndex()); + if(!msg.isInCommandRange()) origin->sendDirectMessage(msg); } } @@ -44,29 +49,30 @@ void ThinSession::addToHistory(protocol::MessagePtr msg) // Request auto-reset when threshold is crossed. const uint autoResetThreshold = history()->effectiveAutoResetThreshold(); - if(autoResetThreshold>0 && m_autoResetRequestStatus == AutoResetState::NotSent && history()->sizeInBytes() > autoResetThreshold) { - log(Log().about(Log::Level::Info, Log::Topic::Status).message( - QString("Autoreset threshold (%1, effectively %2 MB) reached.") - .arg(history()->autoResetThreshold()/(1024.0*1024.0), 0, 'g', 1) - .arg(autoResetThreshold/(1024.0*1024.0), 0, 'g', 1) - )); + if(autoResetThreshold > 0 && + m_autoResetRequestStatus == AutoResetState::NotSent && + history()->sizeInBytes() > autoResetThreshold) { + log(Log() + .about(Log::Level::Info, Log::Topic::Status) + .message( + QString( + "Autoreset threshold (%1, effectively %2 MB) reached.") + .arg( + history()->autoResetThreshold() / (1024.0 * 1024.0), + 0, 'g', 1) + .arg( + autoResetThreshold / (1024.0 * 1024.0), 0, 'g', + 1))); // Legacy alert for Drawpile 2.0.x versions - protocol::ServerReply warning; - warning.type = protocol::ServerReply::SIZELIMITWARNING; - warning.reply["size"] = int(history()->sizeInBytes()); - warning.reply["maxSize"] = int(autoResetThreshold); - - directToAll(protocol::MessagePtr(new protocol::Command(0, warning))); + directToAll(net::ServerReply::makeSizeLimitWarning( + history()->sizeInBytes(), autoResetThreshold)); // New style for Drawpile 2.1.0 and newer - // Autoreset request: send an autoreset query to each logged in operator. - // The user that responds first gets to perform the reset. - protocol::ServerReply resetRequest; - resetRequest.type = protocol::ServerReply::RESETREQUEST; - resetRequest.reply["maxSize"] = int(history()->sizeLimit()); - resetRequest.reply["query"] = true; - protocol::MessagePtr reqMsg { new protocol::Command(0, resetRequest )}; + // Autoreset request: send an autoreset query to each logged in + // operator. The user that responds first gets to perform the reset. + net::Message reqMsg = + net::ServerReply::makeResetRequest(history()->sizeLimit(), true); for(Client *c : clients()) { if(c->isOperator()) @@ -78,10 +84,8 @@ void ThinSession::addToHistory(protocol::MessagePtr msg) // Regular history size status updates if(m_lastStatusUpdate.elapsed() > 10 * 1000) { - protocol::ServerReply status; - status.type = protocol::ServerReply::STATUS; - status.reply["size"] = int(history()->sizeInBytes()); - directToAll(protocol::MessagePtr(new protocol::Command(0, status))); + directToAll( + net::ServerReply::makeStatusUpdate(history()->sizeInBytes())); m_lastStatusUpdate.start(); } } @@ -90,7 +94,9 @@ void ThinSession::cleanupHistoryCache() { int minIdx = history()->lastIndex(); for(const Client *c : clients()) { - minIdx = qMin(static_cast(c)->historyPosition(), minIdx); + minIdx = qMin( + static_cast(c)->historyPosition(), + minIdx); } history()->cleanupBatches(minIdx); } @@ -100,58 +106,67 @@ void ThinSession::readyToAutoReset(int ctxId) Client *c = getClientById(ctxId); if(!c) { // Shouldn't happen - log(Log().about(Log::Level::Error, Log::Topic::RuleBreak).message(QString("Non-existent user %1 sent ready-to-autoreset").arg(ctxId))); + log(Log() + .about(Log::Level::Error, Log::Topic::RuleBreak) + .message(QString("Non-existent user %1 sent ready-to-autoreset") + .arg(ctxId))); return; } if(!c->isOperator()) { // Unlikely to happen normally, but possible if connection is // really slow and user is deopped at just the right moment - log(Log().about(Log::Level::Warn, Log::Topic::RuleBreak).message(QString("User %1 is not an operator, but sent ready-to-autoreset").arg(ctxId))); + log(Log() + .about(Log::Level::Warn, Log::Topic::RuleBreak) + .message(QString("User %1 is not an operator, but sent " + "ready-to-autoreset") + .arg(ctxId))); return; } if(m_autoResetRequestStatus != AutoResetState::Queried) { // Only the first response in handled - log(Log().about(Log::Level::Debug, Log::Topic::Status).message(QString("User %1 was late to respond to an autoreset request").arg(ctxId))); + log(Log() + .about(Log::Level::Debug, Log::Topic::Status) + .message( + QString( + "User %1 was late to respond to an autoreset request") + .arg(ctxId))); return; } - log(Log().about(Log::Level::Info, Log::Topic::Status).message(QString("User %1 responded to autoreset request first").arg(ctxId))); + log(Log() + .about(Log::Level::Info, Log::Topic::Status) + .message(QString("User %1 responded to autoreset request first") + .arg(ctxId))); - protocol::ServerReply resetRequest; - resetRequest.type = protocol::ServerReply::RESETREQUEST; - resetRequest.reply["maxSize"] = int(history()->sizeLimit()); - resetRequest.reply["query"] = false; - c->sendDirectMessage(protocol::MessagePtr { new protocol::Command(0, resetRequest )}); + c->sendDirectMessage( + net::ServerReply::makeResetRequest(history()->sizeLimit(), false)); m_autoResetRequestStatus = AutoResetState::Requested; } void ThinSession::onSessionReset() { - protocol::ServerReply catchup; - catchup.type = protocol::ServerReply::CATCHUP; - catchup.reply["count"] = history()->lastIndex() - history()->firstIndex(); - directToAll(protocol::MessagePtr(new protocol::Command(0, catchup))); - + directToAll(net::ServerReply::makeCatchup( + history()->lastIndex() - history()->firstIndex())); m_autoResetRequestStatus = AutoResetState::NotSent; } void ThinSession::onClientJoin(Client *client, bool host) { - connect(history(), &SessionHistory::newMessagesAvailable, - static_cast(client), &ThinServerClient::sendNextHistoryBatch); + connect( + history(), &SessionHistory::newMessagesAvailable, + static_cast(client), + &ThinServerClient::sendNextHistoryBatch); if(!host) { // Notify the client how many messages to expect (at least) - // The client can use this information to display a progress bar during the login phase - protocol::ServerReply catchup; - catchup.type = protocol::ServerReply::CATCHUP; - catchup.reply["count"] = history()->lastIndex() - history()->firstIndex(); - client->sendDirectMessage(protocol::MessagePtr(new protocol::Command(0, catchup))); + // The client can use this information to display a progress bar during + // the login phase + client->sendDirectMessage(net::ServerReply::makeCatchup( + history()->lastIndex() - history()->firstIndex())); } } } - diff --git a/src/libserver/thinsession.h b/src/libserver/thinsession.h index 4a01e075e3..3bc9bdfc8b 100644 --- a/src/libserver/thinsession.h +++ b/src/libserver/thinsession.h @@ -1,8 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later - #ifndef DP_SERVER_THINSESSION_H #define DP_SERVER_THINSESSION_H - #include "libserver/session.h" namespace server { @@ -13,7 +11,10 @@ namespace server { class ThinSession final : public Session { Q_OBJECT public: - ThinSession(SessionHistory *history, ServerConfig *config, sessionlisting::Announcements *announcements, QObject *parent=nullptr); + ThinSession( + SessionHistory *history, ServerConfig *config, + sessionlisting::Announcements *announcements, + QObject *parent = nullptr); void readyToAutoReset(int ctxId) override; @@ -22,12 +23,12 @@ class ThinSession final : public Session { bool supportsAutoReset() const override { return true; } protected: - void addToHistory(protocol::MessagePtr msg) override; + void addToHistory(const net::Message &msg) override; void onSessionReset() override; void onClientJoin(Client *client, bool host) override; private: - enum class AutoResetState { NotSent, Queried, Requested}; + enum class AutoResetState { NotSent, Queried, Requested }; QElapsedTimer m_lastStatusUpdate; @@ -37,4 +38,3 @@ class ThinSession final : public Session { } #endif - diff --git a/src/libshared/CMakeLists.txt b/src/libshared/CMakeLists.txt index bec75b7b5f..ae3c4da1a4 100644 --- a/src/libshared/CMakeLists.txt +++ b/src/libshared/CMakeLists.txt @@ -5,40 +5,14 @@ target_sources(dpshared PRIVATE listings/announcementapi.h listings/listserverfinder.cpp listings/listserverfinder.h - net/annotation.cpp - net/annotation.h - net/brushes.cpp - net/brushes.h - net/control.cpp - net/control.h - net/image.cpp - net/image.h - net/layer.cpp - net/layer.h net/message.cpp net/message.h net/messagequeue.cpp net/messagequeue.h - net/meta.cpp - net/meta.h - net/meta2.cpp - net/meta2.h - net/opaque.cpp - net/opaque.h net/protover.cpp net/protover.h - net/recording.cpp - net/recording.h - net/textmode.cpp - net/textmode.h - net/undo.cpp - net/undo.h - record/header.cpp - record/header.h - record/reader.cpp - record/reader.h - record/writer.cpp - record/writer.h + net/servercmd.cpp + net/servercmd.h util/filename.cpp util/filename.h util/functionrunnable.cpp @@ -64,6 +38,7 @@ target_link_libraries(dpshared PUBLIC ${QT_PACKAGE_NAME}::Core ${QT_PACKAGE_NAME}::Network + dpengine ) if(ANDROID) diff --git a/src/libshared/net/annotation.cpp b/src/libshared/net/annotation.cpp deleted file mode 100644 index 7564ccdea0..0000000000 --- a/src/libshared/net/annotation.cpp +++ /dev/null @@ -1,218 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -#include "libshared/net/annotation.h" -#include "libshared/net/textmode.h" - -#include - -namespace protocol { - -AnnotationCreate *AnnotationCreate::deserialize(uint8_t ctx, const uchar *data, uint len) -{ - if(len!=14) - return nullptr; - return new AnnotationCreate( - ctx, - qFromBigEndian(data+0), - qFromBigEndian(data+2), - qFromBigEndian(data+6), - qFromBigEndian(data+10), - qFromBigEndian(data+12) - ); -} - -int AnnotationCreate::payloadLength() const -{ - return 2 + 4*2 + 2*2; -} - -int AnnotationCreate::serializePayload(uchar *data) const -{ - uchar *ptr = data; - qToBigEndian(m_id, ptr); ptr += 2; - qToBigEndian(m_x, ptr); ptr += 4; - qToBigEndian(m_y, ptr); ptr += 4; - qToBigEndian(m_w, ptr); ptr += 2; - qToBigEndian(m_h, ptr); ptr += 2; - return ptr - data; -} - -Kwargs AnnotationCreate::kwargs() const -{ - Kwargs kw; - kw["id"] = text::idString(m_id); - kw["x"] = QString::number(m_x); - kw["y"] = QString::number(m_y); - kw["w"] = QString::number(m_w); - kw["h"] = QString::number(m_h); - return kw; -} - -AnnotationCreate *AnnotationCreate::fromText(uint8_t ctx, const Kwargs &kwargs) -{ - return new AnnotationCreate( - ctx, - text::parseIdString16(kwargs["id"]), - kwargs["x"].toInt(), - kwargs["y"].toInt(), - kwargs["w"].toInt(), - kwargs["h"].toInt() - ); -} - -int AnnotationReshape::payloadLength() const -{ - return 2 + 4*2 + 2*2; -} - -AnnotationReshape *AnnotationReshape::deserialize(uint8_t ctx, const uchar *data, uint len) -{ - if(len!=14) - return nullptr; - return new AnnotationReshape( - ctx, - qFromBigEndian(data+0), - qFromBigEndian(data+2), - qFromBigEndian(data+6), - qFromBigEndian(data+10), - qFromBigEndian(data+12) - ); -} - -int AnnotationReshape::serializePayload(uchar *data) const -{ - uchar *ptr = data; - qToBigEndian(m_id, ptr); ptr += 2; - qToBigEndian(m_x, ptr); ptr += 4; - qToBigEndian(m_y, ptr); ptr += 4; - qToBigEndian(m_w, ptr); ptr += 2; - qToBigEndian(m_h, ptr); ptr += 2; - return ptr - data; -} - -Kwargs AnnotationReshape::kwargs() const -{ - Kwargs kw; - kw["id"] = text::idString(m_id); - kw["x"] = QString::number(m_x); - kw["y"] = QString::number(m_y); - kw["w"] = QString::number(m_w); - kw["h"] = QString::number(m_h); - return kw; -} - -AnnotationReshape *AnnotationReshape::fromText(uint8_t ctx, const Kwargs &kwargs) -{ - return new AnnotationReshape( - ctx, - text::parseIdString16(kwargs["id"]), - kwargs["x"].toInt(), - kwargs["y"].toInt(), - kwargs["w"].toInt(), - kwargs["h"].toInt() - ); -} - -AnnotationEdit *AnnotationEdit::deserialize(uint8_t ctx, const uchar *data, uint len) -{ - if(len < 8) - return nullptr; - - return new AnnotationEdit( - ctx, - qFromBigEndian(data+0), - qFromBigEndian(data+2), - *(data+6), - *(data+7), - QByteArray(reinterpret_cast(data)+8, len-8) - ); -} - -int AnnotationEdit::payloadLength() const -{ - return 2 + 4 + 2 + m_text.length(); -} - -int AnnotationEdit::serializePayload(uchar *data) const -{ - uchar *ptr = data; - qToBigEndian(m_id, ptr); ptr += 2; - qToBigEndian(m_bg, ptr); ptr += 4; - *(ptr++) = m_flags; - *(ptr++) = m_border; - memcpy(ptr, m_text.constData(), m_text.length()); - ptr += m_text.length(); - return ptr - data; -} - -Kwargs AnnotationEdit::kwargs() const -{ - Kwargs kw; - kw["id"] = text::idString(m_id); - if(m_bg>0) - kw["bg"] = text::argbString(m_bg); - - if((m_flags&FLAG_PROTECT)) - kw["flags"] = "protect"; - if((m_flags&FLAG_VALIGN_BOTTOM)==FLAG_VALIGN_BOTTOM) - kw["valign"] = "bottom"; - else if((m_flags&FLAG_VALIGN_CENTER)) - kw["valign"] = "center"; - - if(m_border>0) - kw["border"] = QString::number(m_border); - - kw["text"] = text(); - return kw; -} - -AnnotationEdit *AnnotationEdit::fromText(uint8_t ctx, const Kwargs &kwargs) -{ - QStringList flags = kwargs["flags"].split(','); - return new AnnotationEdit( - ctx, - text::parseIdString16(kwargs["id"]), - text::parseColor(kwargs["bg"]), - (flags.contains("protect") ? FLAG_PROTECT : 0) | - (kwargs["valign"]=="bottom" ? FLAG_VALIGN_BOTTOM : 0) | - (kwargs["valign"]=="center" ? FLAG_VALIGN_CENTER : 0), - kwargs["border"].toInt(), - kwargs["text"] - ); -} - -AnnotationDelete *AnnotationDelete::deserialize(uint8_t ctx, const uchar *data, uint len) -{ - if(len != 2) - return nullptr; - return new AnnotationDelete(ctx, qFromBigEndian(data)); -} - -int AnnotationDelete::payloadLength() const -{ - return 2; -} - -int AnnotationDelete::serializePayload(uchar *data) const -{ - uchar *ptr = data; - qToBigEndian(m_id, ptr); ptr += 2; - return ptr-data; -} - -Kwargs AnnotationDelete::kwargs() const -{ - Kwargs kw; - kw["id"] = text::idString(m_id); - return kw; -} - -AnnotationDelete *AnnotationDelete::fromText(uint8_t ctx, const Kwargs &kwargs) -{ - return new AnnotationDelete( - ctx, - text::parseIdString16(kwargs["id"]) - ); -} - -} diff --git a/src/libshared/net/annotation.h b/src/libshared/net/annotation.h deleted file mode 100644 index b351fcdd52..0000000000 --- a/src/libshared/net/annotation.h +++ /dev/null @@ -1,179 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -#ifndef DP_NET_ANNOTATION_H -#define DP_NET_ANNOTATION_H - -#include -#include - -#include "libshared/net/message.h" - -namespace protocol { - -/** - * @brief A command for creating a new annotation. - * - * Annotations are floating text layers. They are drawn over the image layers and - * have no defined stacking order. - * - * The new annotation created with this command is initally empy with a transparent background - */ -class AnnotationCreate final : public Message { -public: - AnnotationCreate(uint8_t ctx, uint16_t id, int32_t x, int32_t y, uint16_t w, uint16_t h) - : Message(MSG_ANNOTATION_CREATE, ctx), m_id(id), m_x(x), m_y(y), m_w(w), m_h(h) - {} - - static AnnotationCreate *deserialize(uint8_t ctx, const uchar *data, uint len); - static AnnotationCreate *fromText(uint8_t ctx, const Kwargs &kwargs); - - /** - * @brief The ID of the newly created annotation - * - * The same rules apply as in layer creation. - * @return annotation ID number - */ - uint16_t id() const { return m_id; } - - //! Alias for id() - uint16_t layer() const override { return m_id; } - - int32_t x() const { return m_x; } - int32_t y() const { return m_y; } - uint16_t w() const { return m_w; } - uint16_t h() const { return m_h; } - - /** - * @brief Check if the ID's namespace portition matches the context ID - * Note. The initial session snapshot may include IDs that do not conform to - * the contextId|annotationId format. - */ - bool isValidId() const { return (id()>>8) == contextId(); } - - QString messageName() const override { return QStringLiteral("newannotation"); } - -protected: - int payloadLength() const override; - int serializePayload(uchar *data) const override; - Kwargs kwargs() const override; - -private: - uint16_t m_id; - int32_t m_x; - int32_t m_y; - uint16_t m_w; - uint16_t m_h; -}; - -/** - * @brief A command for changing annotation position and size - */ -class AnnotationReshape final : public Message { -public: - AnnotationReshape(uint8_t ctx, uint16_t id, int32_t x, int32_t y, uint16_t w, uint16_t h) - : Message(MSG_ANNOTATION_RESHAPE, ctx), m_id(id), m_x(x), m_y(y), m_w(w), m_h(h) - {} - - static AnnotationReshape *deserialize(uint8_t ctx, const uchar *data, uint len); - static AnnotationReshape *fromText(uint8_t ctx, const Kwargs &kwargs); - - uint16_t id() const { return m_id; } - int32_t x() const { return m_x; } - int32_t y() const { return m_y; } - uint16_t w() const { return m_w; } - uint16_t h() const { return m_h; } - - QString messageName() const override { return QStringLiteral("reshapeannotation"); } - -protected: - int payloadLength() const override; - int serializePayload(uchar *data) const override; - Kwargs kwargs() const override; - -private: - uint16_t m_id; - int32_t m_x; - int32_t m_y; - uint16_t m_w; - uint16_t m_h; -}; - -/** - * @brief A command for changing annotation contents - * - * Accepted contents is the subset of HTML understood by QTextDocument - * - * If an annotation is flagged as protected, it cannot be modified by users - * other than the one who created it, or session operators. - */ -class AnnotationEdit final : public Message { -public: - static const uint8_t FLAG_PROTECT = 0x01; // disallow further modifications from other users - static const uint8_t FLAG_VALIGN_CENTER = 0x02; // center vertically - static const uint8_t FLAG_VALIGN_BOTTOM = 0x06; // align to bottom - - AnnotationEdit(uint8_t ctx, uint16_t id, uint32_t bg, uint8_t flags, uint8_t border, const QByteArray &text) - : Message(MSG_ANNOTATION_EDIT, ctx), m_id(id), m_bg(bg), m_flags(flags), m_border(border), m_text(text) - {} - AnnotationEdit(uint8_t ctx, uint16_t id, uint32_t bg, uint8_t flags, uint8_t border, const QString &text) - : AnnotationEdit(ctx, id, bg, flags, border, text.toUtf8()) - {} - - static AnnotationEdit *deserialize(uint8_t ctx, const uchar *data, uint len); - static AnnotationEdit *fromText(uint8_t ctx, const Kwargs &kwargs); - - uint16_t id() const { return m_id; } - uint32_t bg() const { return m_bg; } - uint8_t flags() const { return m_flags; } - uint8_t border() const { return m_border; } /* reserved for future use */ - QString text() const { return QString::fromUtf8(m_text); } - - QString messageName() const override { return QStringLiteral("editannotation"); } - -protected: - int payloadLength() const override; - int serializePayload(uchar *data) const override; - Kwargs kwargs() const override; - -private: - uint16_t m_id; - uint32_t m_bg; - uint8_t m_flags; - uint8_t m_border; - QByteArray m_text; -}; - -/** - * @brief A command for deleting an annotation - * - * Note. Unlike in layer delete command, there is no "merge" option here. - * Merging an annotation is done by rendering the annotation item to - * an image and drawing the image with PutImage command. This is to ensure - * identical rendering on all clients, as due to font and possible rendering - * engine differences, text annotations may appear differently on each client. - */ -class AnnotationDelete final : public Message { -public: - AnnotationDelete(uint8_t ctx, uint16_t id) - : Message(MSG_ANNOTATION_DELETE, ctx), m_id(id) - {} - - static AnnotationDelete *deserialize(uint8_t ctx, const uchar *data, uint len); - static AnnotationDelete *fromText(uint8_t ctx, const Kwargs &kwargs); - - uint16_t id() const { return m_id; } - - QString messageName() const override { return QStringLiteral("deleteannotation"); } - -protected: - int payloadLength() const override; - int serializePayload(uchar *data) const override; - Kwargs kwargs() const override; - -private: - uint16_t m_id; -}; - -} - -#endif diff --git a/src/libshared/net/brushes.cpp b/src/libshared/net/brushes.cpp deleted file mode 100644 index 476b2c20ce..0000000000 --- a/src/libshared/net/brushes.cpp +++ /dev/null @@ -1,439 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -#include "libshared/net/brushes.h" -#include "libshared/net/textmode.h" - -#include -#include - -namespace protocol { - -DrawDabsClassic *DrawDabsClassic::deserialize(uint8_t ctx, const uchar *data, uint len) -{ - if(len < 15) - return nullptr; - - const int dabCount = (len-15) / ClassicBrushDab::LENGTH; - if(uint(dabCount * ClassicBrushDab::LENGTH + 15) != len) - return nullptr; - - DrawDabsClassic *d = new DrawDabsClassic( - ctx, - qFromBigEndian(data+0), - qFromBigEndian(data+2), - qFromBigEndian(data+6), - qFromBigEndian(data+10), - *(data+14) - ); - d->m_dabs.reserve(dabCount); - - data += 15; - - for(int i=0;im_dabs << ClassicBrushDab { - int8_t(*(data+0)), - int8_t(*(data+1)), - qFromBigEndian(data+2), - *(data+4), - *(data+5) - }; - data += ClassicBrushDab::LENGTH; - } - - return d; -} - -int DrawDabsClassic::payloadLength() const -{ - return 2 + 4*3 + 1 + m_dabs.size() * ClassicBrushDab::LENGTH; -} - -int DrawDabsClassic::serializePayload(uchar *data) const -{ - Q_ASSERT(m_dabs.size() <= MAX_DABS); - - uchar *ptr = data; - qToBigEndian(m_layer, ptr); ptr += 2; - qToBigEndian(m_x, ptr); ptr += 4; - qToBigEndian(m_y, ptr); ptr += 4; - qToBigEndian(m_color, ptr); ptr += 4; - *(ptr++) = m_mode; - - for(const ClassicBrushDab &d : m_dabs) { - *(ptr++) = d.x; - *(ptr++) = d.y; - qToBigEndian(d.size, ptr); ptr += 2; - *(ptr++) = d.hardness; - *(ptr++) = d.opacity; - } - - return ptr-data; -} - -bool DrawDabsClassic::payloadEquals(const Message &m) const -{ - const auto &o = static_cast(m); - if(m_dabs.size() != o.m_dabs.size()) - return false; - - if( - m_x != o.m_x || - m_y != o.m_y || - m_color != o.m_color || - m_layer != o.m_layer || - m_mode != o.m_mode - ) - return false; - - for(int i=0;i(dabs); - - if(m_color != ddc.m_color || - m_layer != ddc.m_layer || - m_mode != ddc.m_mode) - return false; - - const int newLength = ddc.dabs().length() + m_dabs.length(); - if(newLength > MAX_DABS) - return false; - - int lastX = m_x; - int lastY = m_y; - for(const auto dab : m_dabs) { - lastX += dab.x; - lastY += dab.y; - } - - auto dab = ddc.dabs().first(); - - const int offsetX = ddc.originX() - lastX + dab.x; - const int offsetY = ddc.originY() - lastY + dab.y; - - if(qAbs(offsetX) > ClassicBrushDab::MAX_XY_DELTA || - qAbs(offsetY) > ClassicBrushDab::MAX_XY_DELTA) - return false; - - m_dabs.reserve(newLength); - - dab.x = offsetX; - dab.y = offsetY; - m_dabs << dab; - - for(int i=1;i(data+0), - qFromBigEndian(data+2), - qFromBigEndian(data+6), - qFromBigEndian(data+10), - *(data+14) - ); - d->m_dabs.reserve(dabCount); - - data += 15; - - for(int i=0;im_dabs << PixelBrushDab { - int8_t(*(data+0)), - int8_t(*(data+1)), - *(data+2), - *(data+3) - }; - data += PixelBrushDab::LENGTH; - } - - return d; -} - -int DrawDabsPixel::payloadLength() const -{ - return 2 + 4*3 + 1 + m_dabs.size() * PixelBrushDab::LENGTH; -} - -int DrawDabsPixel::serializePayload(uchar *data) const -{ - Q_ASSERT(m_dabs.size() <= MAX_DABS); - - uchar *ptr = data; - qToBigEndian(m_layer, ptr); ptr += 2; - qToBigEndian(m_x, ptr); ptr += 4; - qToBigEndian(m_y, ptr); ptr += 4; - qToBigEndian(m_color, ptr); ptr += 4; - *(ptr++) = m_mode; - - for(const PixelBrushDab &d : m_dabs) { - *(ptr++) = d.x; - *(ptr++) = d.y; - *(ptr++) = d.size; - *(ptr++) = d.opacity; - } - - return ptr-data; -} - -bool DrawDabsPixel::payloadEquals(const Message &m) const -{ - const auto &o = static_cast(m); - if(m_dabs.size() != o.m_dabs.size()) - return false; - - if( - m_x != o.m_x || - m_y != o.m_y || - m_color != o.m_color || - m_layer != o.m_layer || - m_mode != o.m_mode - ) - return false; - - for(int i=0;i(dabs); - - if(m_color != ddp.m_color || - m_layer != ddp.m_layer || - m_mode != ddp.m_mode) - return false; - - const int newLength = ddp.dabs().length() + m_dabs.length(); - if(newLength > MAX_DABS) - return false; - - int lastX = m_x; - int lastY = m_y; - for(const auto dab : m_dabs) { - lastX += dab.x; - lastY += dab.y; - } - - auto dab = ddp.dabs().first(); - - const int offsetX = ddp.originX() - lastX + dab.x; - const int offsetY = ddp.originY() - lastY + dab.y; - - if(qAbs(offsetX) > ClassicBrushDab::MAX_XY_DELTA || - qAbs(offsetY) > ClassicBrushDab::MAX_XY_DELTA) - return false; - - m_dabs.reserve(newLength); - - dab.x = offsetX; - dab.y = offsetY; - m_dabs << dab; - - for(int i=1;i -#include - -class QRect; - -namespace protocol { - struct ClassicBrushDab { - int8_t x; // coordinates are relative to previous dab - int8_t y; // (or origin if this is the first dab.) - uint16_t size; // diameter multiplied by 256 - uint8_t hardness; - uint8_t opacity; - - static const int MAX_XY_DELTA = INT8_MAX; - static const int LENGTH = 6; - QString toString() const; - - bool operator!=(const ClassicBrushDab &o) const { - return x != o.x || y != o.y || size != o.size || hardness != o.hardness || opacity != o.opacity; - } - }; - - struct PixelBrushDab { - int8_t x; // coordinates are relative to the previous dab - int8_t y; // (or origin if this is the first dab) - uint8_t size; - uint8_t opacity; - - static const int MAX_XY_DELTA = INT8_MAX; - static const int LENGTH = 4; - QString toString() const; - - bool operator!=(const PixelBrushDab &o) const { - return x != o.x || y != o.y || size != o.size || opacity != o.opacity; - } - }; -} - -Q_DECLARE_TYPEINFO(protocol::ClassicBrushDab, Q_PRIMITIVE_TYPE); -Q_DECLARE_TYPEINFO(protocol::PixelBrushDab, Q_PRIMITIVE_TYPE); - -namespace protocol { - -typedef QVector ClassicBrushDabVector; -typedef QVector PixelBrushDabVector; - -enum class DabShape { - Round, - Square -}; - -/** - * @brief Abstract base class for DrawDabs* messages - */ -class DrawDabs : public Message { -public: - DrawDabs(MessageType type, uint8_t context) - : Message(type, context) - { } - - //! Should these dabs be composited in indirect mode? - virtual bool isIndirect() const = 0; - - //! Get the last coordinates of the last point in the dab vector - virtual QPoint lastPoint() const = 0; - - //! Get the bounding rectangle of the dab vector - virtual QRect bounds() const = 0; - - /** - * @brief Append the given dab message's dabs to this message's dab vector. - * - * If the extension is not possible, this function returns false and - * does not modify the current dab vector. - * - * Possible reasons why extension may fail: - * - given DrawDabs instance is of the wrong type - * - common properties are not the same - * - summed dab array length would be too long - * - distance between lastPoint() and dab.originXY is greater than MAX_XY_DELTA - */ - virtual bool extend(const DrawDabs &dab) = 0; -}; - -/** - * @brief Draw Classic Brush Dabs - * - */ -class DrawDabsClassic final : public DrawDabs { -public: - static const int MAX_DABS = (0xffff - 15) / ClassicBrushDab::LENGTH; - - DrawDabsClassic( - uint8_t ctx, - uint16_t layer, - int32_t originX, int32_t originY, - uint32_t color, - uint8_t blend, - const ClassicBrushDabVector &dabs=ClassicBrushDabVector() - ) - : DrawDabs(MSG_DRAWDABS_CLASSIC, ctx), - m_dabs(dabs), - m_x(originX), m_y(originY), - m_color(color), - m_layer(layer), - m_mode(blend) - { - Q_ASSERT(dabs.size() <= MAX_DABS); - } - - static DrawDabsClassic *deserialize(uint8_t ctx, const uchar *data, uint len); - static DrawDabsClassic *fromText(uint8_t ctx, const Kwargs &kwargs, const QStringList &dabs); - - uint16_t layer() const override { return m_layer; } - int32_t originX() const { return m_x; } // Classic dab coordinates have subpixel precision. - int32_t originY() const { return m_y; } // They are converted to integers by multiplying by 4 - uint32_t color() const { return m_color; } - uint8_t mode() const { return m_mode; } - - // If the color's alpha channel is nonzero, that value is used - // as the opacity of the entire stroke. - bool isIndirect() const override { return (m_color & 0xff000000) > 0; } - - const ClassicBrushDabVector &dabs() const { return m_dabs; } - ClassicBrushDabVector &dabs() { return m_dabs; } - - QString toString() const override; - QString messageName() const override { return QStringLiteral("classicdabs"); } - - QPoint lastPoint() const override; - QRect bounds() const override; - bool extend(const DrawDabs &dab) override; - -protected: - int payloadLength() const override; - int serializePayload(uchar *data) const override; - bool payloadEquals(const Message &m) const override; - Kwargs kwargs() const override { return Kwargs(); } - -private: - ClassicBrushDabVector m_dabs; - int32_t m_x, m_y; - uint32_t m_color; - uint16_t m_layer; - uint8_t m_mode; -}; - - -/** - * @brief Draw Pixel Brush Dabs - * - */ -class DrawDabsPixel final : public DrawDabs { -public: - static const int MAX_DABS = (0xffff - 15) / PixelBrushDab::LENGTH; - - DrawDabsPixel( - DabShape shape, - uint8_t ctx, - uint16_t layer, - int32_t originX, int32_t originY, - uint32_t color, - uint8_t blend, - const PixelBrushDabVector &dabs=PixelBrushDabVector() - ) - : DrawDabs(shape == DabShape::Square ? MSG_DRAWDABS_PIXEL_SQUARE : MSG_DRAWDABS_PIXEL, ctx), - m_dabs(dabs), - m_x(originX), m_y(originY), - m_color(color), - m_layer(layer), - m_mode(blend) - { - Q_ASSERT(dabs.size() <= MAX_DABS); - } - - static DrawDabsPixel *deserialize(DabShape shape, uint8_t ctx, const uchar *data, uint len); - static DrawDabsPixel *fromText(DabShape shape, uint8_t ctx, const Kwargs &kwargs, const QStringList &dabs); - - uint16_t layer() const override { return m_layer; } - int32_t originX() const { return m_x; } - int32_t originY() const { return m_y; } - uint32_t color() const { return m_color; } // If the alpha channel is set, the dabs are composited indirectly - uint8_t mode() const { return m_mode; } - bool isSquare() const { return type() == MSG_DRAWDABS_PIXEL_SQUARE; } - - // If the color's alpha channel is nonzero, that value is used - // as the opacity of the entire stroke. - bool isIndirect() const override { return (m_color & 0xff000000) > 0; } - - const PixelBrushDabVector &dabs() const { return m_dabs; } - PixelBrushDabVector &dabs() { return m_dabs; } - - QString toString() const override; - QString messageName() const override { return isSquare() ? QStringLiteral("squarepixeldabs") : QStringLiteral("pixeldabs"); } - - QPoint lastPoint() const override; - QRect bounds() const override; - bool extend(const DrawDabs &dab) override; - -protected: - int payloadLength() const override; - int serializePayload(uchar *data) const override; - bool payloadEquals(const Message &m) const override; - Kwargs kwargs() const override { return Kwargs(); } - -private: - PixelBrushDabVector m_dabs; - int32_t m_x, m_y; - uint32_t m_color; - uint16_t m_layer; - uint8_t m_mode; -}; - -/** - * @brief Pen up command - * - * The pen up command signals the end of a stroke. In indirect drawing mode, it causes - * indirect dabs (by this user) to be merged to their parent layers. - */ -class PenUp : public ZeroLengthMessage { -public: - PenUp(uint8_t ctx) : ZeroLengthMessage(MSG_PEN_UP, ctx) {} - - QString messageName() const override { return QStringLiteral("penup"); } -}; - -} - -#endif diff --git a/src/libshared/net/control.cpp b/src/libshared/net/control.cpp deleted file mode 100644 index 06a132881a..0000000000 --- a/src/libshared/net/control.cpp +++ /dev/null @@ -1,187 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -#include "libshared/net/control.h" - -#include - -#include -#include - -namespace protocol { - -ServerCommand ServerCommand::fromJson(const QJsonDocument &doc) -{ - const QJsonObject o = doc.object(); - ServerCommand c; - c.cmd = o.value("cmd").toString(); - c.args = o.value("args").toArray(); - c.kwargs = o.value("kwargs").toObject(); - return c; -} - -QJsonDocument ServerCommand::toJson() const -{ - QJsonObject o; - o["cmd"] = cmd; - if(!args.isEmpty()) - o["args"] = args; - if(!kwargs.isEmpty()) - o["kwargs"] = kwargs; - return QJsonDocument(o); -} - -ServerReply ServerReply::fromJson(const QJsonDocument &doc) -{ - const QJsonObject o = doc.object(); - ServerReply r; - QString typestr = o.value("type").toString(); - - if(typestr == "login") - r.type = LOGIN; - else if(typestr == "msg") - r.type = MESSAGE; - else if(typestr == "alert") - r.type = ALERT; - else if(typestr == "error") - r.type = ERROR; - else if(typestr == "result") - r.type = RESULT; - else if(typestr == "log") - r.type = LOG; - else if(typestr == "sessionconf") - r.type = SESSIONCONF; - else if(typestr == "sizelimit") - r.type = SIZELIMITWARNING; - else if(typestr == "status") - r.type = STATUS; - else if(typestr == "reset") - r.type = RESET; - else if(typestr == "autoreset") - r.type = RESETREQUEST; - else if(typestr == "catchup") - r.type = CATCHUP; - else - r.type = UNKNOWN; - - r.message = o.value("message").toString(); - r.reply = o; - return r; -} - -QJsonDocument ServerReply::toJson() const -{ - QJsonObject o = reply; - QString typestr; - switch(type) { - case UNKNOWN: break; - case LOGIN: typestr=QStringLiteral("login"); break; - case MESSAGE: typestr=QStringLiteral("msg"); break; - case ALERT: typestr=QStringLiteral("alert"); break; - case ERROR: typestr=QStringLiteral("error"); break; - case RESULT: typestr=QStringLiteral("result"); break; - case LOG: typestr=QStringLiteral("log"); break; - case SESSIONCONF: typestr=QStringLiteral("sessionconf"); break; - case SIZELIMITWARNING: typestr=QStringLiteral("sizelimit"); break; - case STATUS: typestr=QStringLiteral("status"); break; - case RESET: typestr=QStringLiteral("reset"); break; - case CATCHUP: typestr=QStringLiteral("catchup"); break; - case RESETREQUEST: typestr=QStringLiteral("autoreset"); break; - } - o["type"] = typestr; - - if(!o.contains("message")) - o["message"] = message; - return QJsonDocument(o); -} - -MessagePtr Command::error(const QString &message) -{ - ServerReply sr; - sr.type = ServerReply::ERROR; - sr.message = message; - return MessagePtr(new Command(0, sr)); -} - -Command *Command::deserialize(uint8_t ctxid, const uchar *data, uint len) -{ - return new Command(ctxid, QByteArray(reinterpret_cast(data), len)); -} - -int Command::serializePayload(uchar *data) const -{ - const int len = payloadLength(); - memcpy(data, m_msg.constData(), len); - return len; -} - -int Command::payloadLength() const -{ - return qMin(0xffff, m_msg.length()); -} - -QJsonDocument Command::doc() const -{ - QJsonParseError e; - QJsonDocument d = QJsonDocument::fromJson(m_msg, &e); - if(e.error != QJsonParseError::NoError) { - qWarning() << "JSON parse error:" << e.errorString(); - } - return d; -} - -QString Command::toString() const -{ - return QStringLiteral("# Command"); -} - -Disconnect *Disconnect::deserialize(uint8_t ctx, const uchar *data, uint len) -{ - if(len<1) - return nullptr; - return new Disconnect(ctx, Reason(*data), QByteArray(reinterpret_cast(data)+1, len-1)); -} - -int Disconnect::serializePayload(uchar *data) const -{ - uchar *ptr = data; - *(ptr++) = _reason; - memcpy(ptr, _message.constData(), _message.length()); - ptr += _message.length(); - return ptr - data; -} - -int Disconnect::payloadLength() const -{ - return 1 + _message.length(); -} - -QString Disconnect::toString() const -{ - return QStringLiteral("# Disconnected: ") + message(); -} - -Ping *Ping::deserialize(uint8_t ctx, const uchar *data, int len) -{ - if(len!=1) - return nullptr; - return new Ping(ctx, *data); -} - -int Ping::payloadLength() const -{ - return 1; -} - -int Ping::serializePayload(uchar *data) const -{ - *data = m_isPong; - return 1; -} - -QString Ping::toString() const -{ - return QStringLiteral("# ") + messageName(); -} - -} - diff --git a/src/libshared/net/control.h b/src/libshared/net/control.h deleted file mode 100644 index 1816ec06ca..0000000000 --- a/src/libshared/net/control.h +++ /dev/null @@ -1,166 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -#ifndef DP_NET_CTRL_H -#define DP_NET_CTRL_H - -#include "libshared/net/message.h" - -#include -#include -#include - -namespace protocol { - -/** - * @brief A command sent to the server using the Command message - */ -struct ServerCommand { - QString cmd; - QJsonArray args; - QJsonObject kwargs; - - static ServerCommand fromJson(const QJsonDocument &doc); - QJsonDocument toJson() const; -}; - -/** - * @brief A reply or notification from the server received with the Command message - */ -struct ServerReply { - enum { - UNKNOWN, - LOGIN, // used during the login phase - MESSAGE, // general chat type notifcation message - ALERT, // urgen notification message - ERROR, // error occurred - RESULT, // comand result - LOG, // server log message - SESSIONCONF, // session configuration update - SIZELIMITWARNING, // session history size nearing limit (deprecated) - STATUS, // Periodic status update - RESET, // session reset state - CATCHUP, // number of messages queued for upload (use for progress bars) - RESETREQUEST // request client to perform a reset - } type; - QString message; - QJsonObject reply; - - static ServerReply fromJson(const QJsonDocument &doc); - QJsonDocument toJson() const; -}; - -/** - * @brief Server command message - * - * This is a general purpose message for sending commands to the server - * and receiving replies. This is used for (among other things): - * - the login handshake - * - setting session parameters (e.g. max user count and password) - * - sending administration commands (e.g. kick user) - */ -class Command final : public Message { -public: - Command(uint8_t ctx, const QByteArray &msg) : Message(MSG_COMMAND, ctx), m_msg(msg) {} - Command(uint8_t ctx, const QJsonDocument &doc) : Command(ctx, doc.toJson(QJsonDocument::Compact)) {} - template Command(uint8_t ctx, const T &t) : Command(ctx, t.toJson()) { } - - static Command *deserialize(uint8_t ctxid, const uchar *data, uint len); - - //! Convenience function: make an ERROR type reply message - static MessagePtr error(const QString &message); - - //! Check is message payload is too big to be sent - bool isOversize() const { return m_msg.length() > 0xffff; } - - QJsonDocument doc() const; - ServerCommand cmd() const { return ServerCommand::fromJson(doc()); } - ServerReply reply() const { return ServerReply::fromJson(doc()); } - - QString toString() const override; - QString messageName() const override { return QStringLiteral("command"); } - -protected: - int payloadLength() const override; - int serializePayload(uchar *data) const override; - Kwargs kwargs() const override { return Kwargs(); } - -private: - QByteArray m_msg; -}; - -/** - * @brief Disconnect notification - * - * This message is used when closing the connection gracefully. The message queue - * will automatically close the socket after sending this message. - */ -class Disconnect final : public Message { -public: - enum Reason { - ERROR, // client/server error - KICK, // user kicked by session operator - SHUTDOWN, // client/server closed - OTHER // other unspecified error - }; - - Disconnect(uint8_t ctx, Reason reason, const QString &message) : Message(MSG_DISCONNECT, ctx), - _reason(reason), _message(message.toUtf8()) { } - - static Disconnect *deserialize(uint8_t ctx, const uchar *data, uint len); - - /** - * Get the reason for the disconnection - */ - Reason reason() const { return _reason; } - - /** - * Get the disconnect message - * - * When reason is KICK, this is the name of the operator who kicked this user. - */ - QString message() const { return QString::fromUtf8(_message); } - - QString toString() const override; - QString messageName() const override { return QStringLiteral("disconnect"); } - -protected: - int payloadLength() const override; - int serializePayload(uchar *data) const override; - Kwargs kwargs() const override { return Kwargs(); } - -private: - Reason _reason; - QByteArray _message; -}; - -/** - * @brief Ping message - * - * This is used for latency measurement as well as a keepalive. Normally, the client - * should be the one to send the ping messages. - * - * The server should return with a Ping with the pong message setenv() - */ -class Ping final : public Message { -public: - Ping(uint8_t ctx, bool pong) : Message(MSG_PING, ctx), m_isPong(pong) { } - - static Ping *deserialize(uint8_t ctx, const uchar *data, int len); - - bool isPong() const { return m_isPong; } - - QString toString() const override; - QString messageName() const override { return m_isPong ? QStringLiteral("pong") : QStringLiteral("ping"); } - -protected: - int payloadLength() const override; - int serializePayload(uchar *data) const override; - Kwargs kwargs() const override { return Kwargs(); } - -private: - bool m_isPong; -}; - -} - -#endif diff --git a/src/libshared/net/image.cpp b/src/libshared/net/image.cpp deleted file mode 100644 index aa277a49cb..0000000000 --- a/src/libshared/net/image.cpp +++ /dev/null @@ -1,441 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -#include "libshared/net/image.h" -#include "libshared/net/textmode.h" - -#include - -namespace protocol { - -PutImage *PutImage::deserialize(uint8_t ctx, const uchar *data, uint len) -{ - if(len < 19) - return nullptr; - - return new PutImage( - ctx, - qFromBigEndian(data+0), - *(data+2), - qFromBigEndian(data+3), - qFromBigEndian(data+7), - qFromBigEndian(data+11), - qFromBigEndian(data+15), - QByteArray(reinterpret_cast(data)+19, len-19) - ); -} - -int PutImage::payloadLength() const -{ - return 3 + 4*4 + m_image.size(); -} - -int PutImage::serializePayload(uchar *data) const -{ - uchar *ptr = data; - qToBigEndian(m_layer, ptr); ptr += 2; - *(ptr++) = m_mode; - qToBigEndian(m_x, ptr); ptr += 4; - qToBigEndian(m_y, ptr); ptr += 4; - qToBigEndian(m_w, ptr); ptr += 4; - qToBigEndian(m_h, ptr); ptr += 4; - memcpy(ptr, m_image.constData(), m_image.length()); - ptr += m_image.length(); - return ptr-data; -} - -bool PutImage::payloadEquals(const Message &m) const -{ - const PutImage &p = static_cast(m); - return - layer() == p.layer() && - blendmode() == p.blendmode() && - x() == p.x() && - y() == p.y() && - width() == p.width() && - height() == p.height() && - image() == p.image(); -} - -// Split the base64 encoded image data into multiple lines -// so the text file is nicer to look at -static QString splitToColumns(const QByteArray text, int cols) -{ - QString out; - out.reserve(text.length() + text.length()/cols); - out += QString::fromUtf8(text.left(cols)); - for(int i=cols;iMAX_LEN) - return nullptr; - - return new PutImage( - ctx, - text::parseIdString16(kwargs["layer"]), - kwargs.value("mode", "1").toInt(), - kwargs["x"].toInt(), - kwargs["y"].toInt(), - kwargs["w"].toInt(), - kwargs["h"].toInt(), - img - ); -} - -static QByteArray colorByteArray(quint32 c) -{ - QByteArray ba(4, 0); - qToBigEndian(c, ba.data()); - return ba; -} - -PutTile::PutTile(uint8_t ctx, uint16_t layer, uint8_t sublayer, uint16_t col, uint16_t row, uint16_t repeat, uint32_t color) - : PutTile(ctx, layer, sublayer, col, row, repeat, colorByteArray(color)) -{ -} - -PutTile *PutTile::deserialize(uint8_t ctx, const uchar *data, uint len) -{ - if(len < 13) - return nullptr; - - return new PutTile( - ctx, - qFromBigEndian(data+0), - *(data+2), - qFromBigEndian(data+3), - qFromBigEndian(data+5), - qFromBigEndian(data+7), - QByteArray(reinterpret_cast(data)+9, len-9) - ); -} - -int PutTile::payloadLength() const -{ - return 9 + m_image.length(); -} - -int PutTile::serializePayload(uchar *data) const -{ - uchar *ptr = data; - qToBigEndian(m_layer, ptr); ptr += 2; - *(ptr++) = m_sublayer; - qToBigEndian(m_col, ptr); ptr += 2; - qToBigEndian(m_row, ptr); ptr += 2; - qToBigEndian(m_repeat, ptr); ptr += 2; - - memcpy(ptr, m_image.constData(), m_image.length()); - ptr += m_image.length(); - - return ptr-data; -} - -uint32_t PutTile::color() const -{ - Q_ASSERT(m_image.length()==4); - return qFromBigEndian(m_image.constData()); -} - -bool PutTile::payloadEquals(const Message &m) const -{ - const PutTile &p = static_cast(m); - return - layer() == p.layer() && - sublayer() == p.sublayer() && - column() == p.column() && - row() == p.row() && - repeat() == p.repeat() && - image() == p.image(); -} - -Kwargs PutTile::kwargs() const -{ - Kwargs kw; - kw["layer"] = text::idString(m_layer); - if(m_sublayer>0) - kw["sublayer"] = QString::number(m_sublayer); - kw["row"] = QString::number(m_row); - kw["col"] = QString::number(m_col); - if(m_repeat>0) - kw["repeat"] = QString::number(m_repeat); - if(isSolidColor()) - kw["color"] = text::argbString(color()); - else - kw["img"] = splitToColumns(m_image.toBase64(), 70); - - return kw; -} - -PutTile *PutTile::fromText(uint8_t ctx, const Kwargs &kwargs) -{ - QByteArray img; - if(kwargs.contains("color")) { - img = colorByteArray(text::parseColor(kwargs["color"])); - - } else { - img = QByteArray::fromBase64(kwargs["img"].toUtf8()); - if(img.length()<=4) - return nullptr; - } - - return new PutTile( - ctx, - text::parseIdString16(kwargs["layer"]), - kwargs["sublayer"].toInt(), - kwargs["col"].toInt(), - kwargs["row"].toInt(), - kwargs["repeat"].toInt(), - img - ); -} - -CanvasBackground::CanvasBackground(uint8_t ctx, uint32_t color) - : CanvasBackground(ctx, colorByteArray(color)) -{ -} - -CanvasBackground *CanvasBackground::deserialize(uint8_t ctx, const uchar *data, uint len) -{ - if(len < 4) - return nullptr; - - return new CanvasBackground( - ctx, - QByteArray(reinterpret_cast(data), len) - ); -} - -int CanvasBackground::payloadLength() const -{ - return m_image.length(); -} - -int CanvasBackground::serializePayload(uchar *data) const -{ - memcpy(data, m_image.constData(), m_image.length()); - return m_image.length(); -} - -uint32_t CanvasBackground::color() const -{ - Q_ASSERT(m_image.length()==4); - return qFromBigEndian(m_image.constData()); -} - -bool CanvasBackground::payloadEquals(const Message &m) const -{ - const CanvasBackground &p = static_cast(m); - return m_image == p.m_image; -} - -Kwargs CanvasBackground::kwargs() const -{ - Kwargs kw; - if(isSolidColor()) - kw["color"] = text::argbString(color()); - else - kw["img"] = splitToColumns(m_image.toBase64(), 70); - - return kw; -} - -CanvasBackground *CanvasBackground::fromText(uint8_t ctx, const Kwargs &kwargs) -{ - QByteArray img; - if(kwargs.contains("color")) { - img = colorByteArray(text::parseColor(kwargs["color"])); - - } else { - img = QByteArray::fromBase64(kwargs["img"].toUtf8()); - if(img.length()<=4) - return nullptr; - } - - return new CanvasBackground(ctx, img); -} - -FillRect *FillRect::deserialize(uint8_t ctx, const uchar *data, uint len) -{ - if(len != 23) - return nullptr; - - return new FillRect( - ctx, - qFromBigEndian(data+0), - *(data+2), - qFromBigEndian(data+3), - qFromBigEndian(data+7), - qFromBigEndian(data+11), - qFromBigEndian(data+15), - qFromBigEndian(data+19) - ); -} - -int FillRect::payloadLength() const -{ - return 3 + 4*4 + 4; -} - -int FillRect::serializePayload(uchar *data) const -{ - uchar *ptr = data; - qToBigEndian(m_layer, ptr); ptr += 2; - *(ptr++) = m_blend; - qToBigEndian(m_x, ptr); ptr += 4; - qToBigEndian(m_y, ptr); ptr += 4; - qToBigEndian(m_w, ptr); ptr += 4; - qToBigEndian(m_h, ptr); ptr += 4; - qToBigEndian(m_color, ptr); ptr += 4; - - return ptr-data; -} - -Kwargs FillRect::kwargs() const -{ - Kwargs kw; - kw["layer"] = text::idString(m_layer); - kw["blend"] = QString::number(m_blend); - kw["color"] = text::argbString(m_color); - kw["x"] = QString::number(m_x); - kw["y"] = QString::number(m_y); - kw["w"] = QString::number(m_w); - kw["h"] = QString::number(m_h); - return kw; -} - -FillRect *FillRect::fromText(uint8_t ctx, const Kwargs &kwargs) -{ - return new FillRect( - ctx, - text::parseIdString16(kwargs["layer"]), - kwargs.value("blend", "1").toInt(), - kwargs["x"].toInt(), - kwargs["y"].toInt(), - kwargs["w"].toInt(), - kwargs["h"].toInt(), - text::parseColor(kwargs["color"]) - ); -} - -MoveRegion *MoveRegion::deserialize(uint8_t ctx, const uchar *data, uint len) -{ - if(len < (2 + 4*4 + 8*4)) - return nullptr; - - return new MoveRegion( - ctx, - qFromBigEndian(data+0), // layer ID - qFromBigEndian(data+2), // source bounding rect - qFromBigEndian(data+6), - qFromBigEndian(data+10), - qFromBigEndian(data+14), - qFromBigEndian(data+18), // target 1 - qFromBigEndian(data+22), - qFromBigEndian(data+26), // target 2 - qFromBigEndian(data+30), - qFromBigEndian(data+34), // target 3 - qFromBigEndian(data+38), - qFromBigEndian(data+42), // target 4 - qFromBigEndian(data+46), - QByteArray(reinterpret_cast(data)+50, len-50) // source mask - ); -} - -int MoveRegion::payloadLength() const -{ - return 2 + 4*4 + 8*4 + m_mask.size(); -} - -int MoveRegion::serializePayload(uchar *data) const -{ - uchar *ptr = data; - qToBigEndian(m_layer, ptr); ptr += 2; - qToBigEndian(m_bx, ptr); ptr += 4; - qToBigEndian(m_by, ptr); ptr += 4; - qToBigEndian(m_bw, ptr); ptr += 4; - qToBigEndian(m_bh, ptr); ptr += 4; - - qToBigEndian(m_x1, ptr); ptr += 4; - qToBigEndian(m_y1, ptr); ptr += 4; - qToBigEndian(m_x2, ptr); ptr += 4; - qToBigEndian(m_y2, ptr); ptr += 4; - qToBigEndian(m_x3, ptr); ptr += 4; - qToBigEndian(m_y3, ptr); ptr += 4; - qToBigEndian(m_x4, ptr); ptr += 4; - qToBigEndian(m_y4, ptr); ptr += 4; - - memcpy(ptr, m_mask.constData(), m_mask.length()); - ptr += m_mask.length(); - return ptr-data; -} - -Kwargs MoveRegion::kwargs() const -{ - Kwargs kw; - kw["layer"] = text::idString(m_layer); - kw["bx"] = QString::number(m_bx); - kw["by"] = QString::number(m_by); - kw["bw"] = QString::number(m_bw); - kw["bh"] = QString::number(m_bh); - - kw["x1"] = QString::number(m_x1); - kw["y1"] = QString::number(m_y1); - kw["x2"] = QString::number(m_x2); - kw["y2"] = QString::number(m_y2); - kw["x3"] = QString::number(m_x3); - kw["y3"] = QString::number(m_y3); - kw["x4"] = QString::number(m_x4); - kw["y4"] = QString::number(m_y4); - - if(!m_mask.isEmpty()) - kw["mask"] = splitToColumns(m_mask.toBase64(), 70); - - return kw; -} - -MoveRegion *MoveRegion::fromText(uint8_t ctx, const Kwargs &kwargs) -{ - QByteArray mask = QByteArray::fromBase64(kwargs["mask"].toUtf8()); - if(mask.length()>MAX_LEN) - return nullptr; - - return new MoveRegion( - ctx, - text::parseIdString16(kwargs["layer"]), - kwargs["bx"].toInt(), - kwargs["by"].toInt(), - kwargs["bw"].toInt(), - kwargs["bh"].toInt(), - kwargs["x1"].toInt(), - kwargs["y1"].toInt(), - kwargs["x2"].toInt(), - kwargs["y2"].toInt(), - kwargs["x3"].toInt(), - kwargs["y3"].toInt(), - kwargs["x4"].toInt(), - kwargs["y4"].toInt(), - mask - ); -} - -} diff --git a/src/libshared/net/image.h b/src/libshared/net/image.h deleted file mode 100644 index 7268e20adb..0000000000 --- a/src/libshared/net/image.h +++ /dev/null @@ -1,325 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -#ifndef DP_NET_IMAGE_H -#define DP_NET_IMAGE_H - -#include "libshared/net/message.h" - -#include -#include -#include - -namespace protocol { - -/** - * @brief Draw a bitmap onto a layer - * - * This is used when initializing the canvas from an existing file - * and when pasting images. - * - * All brush/layer blending modes are supported. - * - * The image data is DEFLATEd 32bit premultiplied ARGB data. - * - * Note that since the message length is fairly limited, a - * large image may have to be divided into multiple PutImage - * commands. - */ -class PutImage final : public Message { -public: - //! Maximum length of image data array - static const int MAX_LEN = 0xffff - 19; - - PutImage(uint8_t ctx, uint16_t layer, uint8_t mode, uint32_t x, uint32_t y, uint32_t w, uint32_t h, const QByteArray &image) - : Message(MSG_PUTIMAGE, ctx), m_layer(layer), m_mode(mode), m_x(x), m_y(y), m_w(w), m_h(h), m_image(image) - { - Q_ASSERT(image.length() <= MAX_LEN); - } - - static PutImage *deserialize(uint8_t ctx, const uchar *data, uint len); - static PutImage *fromText(uint8_t ctx, const Kwargs &kwargs); - - uint16_t layer() const override { return m_layer; } - uint8_t blendmode() const { return m_mode; } - uint32_t x() const { return m_x; } - uint32_t y() const { return m_y; } - uint32_t width() const { return m_w; } - uint32_t height() const { return m_h; } - const QByteArray &image() const { return m_image; } - - QString messageName() const override { return QStringLiteral("putimage"); } - -protected: - int payloadLength() const override; - int serializePayload(uchar *data) const override; - bool payloadEquals(const Message &m) const override; - Kwargs kwargs() const override; - -private: - uint16_t m_layer; - uint8_t m_mode; - uint32_t m_x; - uint32_t m_y; - uint32_t m_w; - uint32_t m_h; - QByteArray m_image; -}; - -/** - * @brief Set the content of a tile - * - * Unlike PutImage, this replaces an entire tile directly without any blending. - * This command is typically used during canvas initialization to set the initial content. - * - * PutTiles can be targeted at sublayers as well. This is used when generating a reset image - * with incomplete indirect strokes. Sending a PenUp command will merge the sublayer. - */ -class PutTile final : public Message { -public: - /** - * @brief Construct a solid color PutTile - * @param ctx context ID - * @param layer target layer - * @param sublayer sublayer (0 means no sublayer) - * @param col tile column - * @param row tile row - * @param repeat put this many extra tiles - * @param color tile fill color ARGB (unpremultiplied) - */ - PutTile(uint8_t ctx, uint16_t layer, uint8_t sublayer, uint16_t col, uint16_t row, uint16_t repeat, uint32_t color); - - /** - * @brief Construct a PutTile - * @param ctx context ID - * @param layer target layer - * @param sublayer sublayer (0 means no sublayer) - * @param col tile column - * @param row tile row - * @param repeat put this many extra tiles - * @param image tile content. Uncompressed length must be 64x64x4 - */ - PutTile(uint8_t ctx, uint16_t layer, uint8_t sublayer, uint16_t col, uint16_t row, uint16_t repeat, const QByteArray &image) - : Message(MSG_PUTTILE, ctx), m_layer(layer), m_col(col), m_row(row), m_repeat(repeat), m_sublayer(sublayer), m_image(image) - { - // Note: an uncompressed tile is only 16KB, so this should never be - // anywhere near this long - Q_ASSERT(image.length() <= 0xffff - 8); - Q_ASSERT(image.length() >= 4); - } - - static PutTile *deserialize(uint8_t ctx, const uchar *data, uint len); - static PutTile *fromText(uint8_t ctx, const Kwargs &kwargs); - - uint16_t layer() const override { return m_layer; } - uint8_t sublayer() const { return m_sublayer; } - uint16_t column() const { return m_col; } - uint16_t row() const { return m_row; } - uint16_t repeat() const { return m_repeat; } - uint32_t color() const; - const QByteArray &image() const { return m_image; } - - bool isSolidColor() const { return m_image.length() == 4; } - - QString messageName() const override { return QStringLiteral("puttile"); } - -protected: - int payloadLength() const override; - int serializePayload(uchar *data) const override; - bool payloadEquals(const Message &m) const override; - Kwargs kwargs() const override; - -private: - uint16_t m_layer; - uint16_t m_col; - uint16_t m_row; - uint16_t m_repeat; - uint8_t m_sublayer; - QByteArray m_image; -}; - -/** - * @brief Set the canvas background - * - */ -class CanvasBackground final : public Message { -public: - /** - * @brief Construct a solid color background - * @param ctx context ID - * @param color background color ARGB (unpremultiplied) - */ - CanvasBackground(uint8_t ctx, uint32_t color); - - /** - * @brief Construct a pattern background - * @param ctx context ID - * @param image tile content. Uncompressed length must be 64x64x4 - */ - CanvasBackground(uint8_t ctx, const QByteArray &image) - : Message(MSG_CANVAS_BACKGROUND, ctx), m_image(image) - { - // Note: an uncompressed tile is only 16KB, so this should never be - // anywhere near this long - Q_ASSERT(image.length() <= 0xffff - 8); - Q_ASSERT(image.length() >= 4); - } - - static CanvasBackground *deserialize(uint8_t ctx, const uchar *data, uint len); - static CanvasBackground *fromText(uint8_t ctx, const Kwargs &kwargs); - - uint32_t color() const; - const QByteArray &image() const { return m_image; } - - bool isSolidColor() const { return m_image.length() == 4; } - - QString messageName() const override { return QStringLiteral("background"); } - -protected: - int payloadLength() const override; - int serializePayload(uchar *data) const override; - bool payloadEquals(const Message &m) const override; - Kwargs kwargs() const override; - -private: - QByteArray m_image; -}; - - -/** - * @brief Fill a rectangle with solid color - * - * All brush blending modes are supported - */ -class FillRect final : public Message { -public: - FillRect(uint8_t ctx, uint16_t layer, uint8_t blend, uint32_t x, uint32_t y, uint32_t w, uint32_t h, uint32_t color) - : Message(MSG_FILLRECT, ctx), m_layer(layer), m_blend(blend), m_x(x), m_y(y), m_w(w), m_h(h), m_color(color) - { - } - - static FillRect *deserialize(uint8_t ctx, const uchar *data, uint len); - static FillRect *fromText(uint8_t ctx, const Kwargs &kwargs); - - uint16_t layer() const override { return m_layer; } - uint8_t blend() const { return m_blend; } - uint32_t x() const { return m_x; } - uint32_t y() const { return m_y; } - uint32_t width() const { return m_w; } - uint32_t height() const { return m_h; } - uint32_t color() const { return m_color; } - - QString messageName() const override { return QStringLiteral("fillrect"); } - -protected: - int payloadLength() const override; - int serializePayload(uchar *data) const override; - Kwargs kwargs() const override; - -private: - uint16_t m_layer; - uint8_t m_blend; - uint32_t m_x; - uint32_t m_y; - uint32_t m_w; - uint32_t m_h; - uint32_t m_color; -}; - -/** - * @brief Move (and transform) a region of a layer. - * - * This is used to implement selection moving. It is equivalent - * to doing two PutImages: the first to mask away the original - * selection and the other to paste the selection to a new location. - * - * This command packages that into a single action that is more - * bandwidth efficient and can be used even when PutImages in general - * are locked, since it's not introducing any new pixels onto the canvas. - * - * Internally, the paint engine performs the following steps: - * 1. Copy selected pixels to a buffer - * 2. Erase selected pixels from the layer - * 3. Composite transformed buffer onto the layer - * - * The pixel selection is determined by the mask bitmap. The mask - * is DEFLATEd 1 bit per pixel bitmap data. - * For axis aligned rectangle selections, no bitmap is necessary. - */ -class MoveRegion final : public Message { -public: - //! Maximum length of the mask (it's compressed 1bpp image data, typically representing a simple polygon. ~64k should be more than plenty) - static const int MAX_LEN = 0xffff - 50; - - /** - * @brief Construct a MoveRegion message - * @param ctx context ID - * @param layer layer ID - * @param bx source bounding rect X - * @param by source bounding rect Y - * @param bw source bounding rect width - * @param bh source bounding rect height - * @param x1 target quad vertex 1 X - * @param y1 target quad vertex 1 Y - * @param x2 target quad vertex 2 X - * @param y2 target quad vertex 2 Y - * @param x3 target quad vertex 3 X - * @param y3 target quad vertex 3 Y - * @param x4 target quad vertex 4 X - * @param y4 target quad vertex 4 Y - * @param mask source mask bitmap - */ - MoveRegion(uint8_t ctx, uint16_t layer, int32_t bx, int32_t by, int32_t bw, int32_t bh, int32_t x1, int32_t y1, int32_t x2, int32_t y2, int32_t x3, int32_t y3, int32_t x4, int32_t y4, const QByteArray &mask) - : Message(MSG_REGION_MOVE, ctx), m_layer(layer), m_bx(bx), m_by(by), m_bw(bw), m_bh(bh), - m_x1(x1), m_y1(y1), m_x2(x2), m_y2(y2), m_x3(x3), m_y3(y3), m_x4(x4), m_y4(y4), - m_mask(mask) - { } - - static MoveRegion *deserialize(uint8_t ctx, const uchar *data, uint len); - static MoveRegion *fromText(uint8_t ctx, const Kwargs &kwargs); - - uint16_t layer() const override { return m_layer; } - int32_t bx() const { return m_bx; } - int32_t by() const { return m_by; } - int32_t bw() const { return m_bw; } - int32_t bh() const { return m_bh; } - - int32_t x1() const { return m_x1; } - int32_t y1() const { return m_y1; } - - int32_t x2() const { return m_x2; } - int32_t y2() const { return m_y2; } - - int32_t x3() const { return m_x3; } - int32_t y3() const { return m_y3; } - - int32_t x4() const { return m_x4; } - int32_t y4() const { return m_y4; } - - QByteArray mask() const { return m_mask; } - - QString messageName() const override { return QStringLiteral("moveregion"); } - - QRect sourceBounds() const { return QRect(bx(), by(), bw(), bh()); } - QRect targetBounds() const { - const int left = qMin(qMin(x1(), x2()), qMin(x3(), x4())); - const int right = qMax(qMax(x1(), x2()), qMax(x3(), x4())); - const int top = qMin(qMin(y1(), y2()), qMin(y3(), y4())); - const int bottom = qMax(qMax(y1(), y2()), qMax(y3(), y4())); - return QRect(QPoint(left, top), QPoint(right, bottom)); - } - -protected: - int payloadLength() const override; - int serializePayload(uchar *data) const override; - Kwargs kwargs() const override; - -private: - uint16_t m_layer; - int32_t m_bx, m_by, m_bw, m_bh; - int32_t m_x1, m_y1, m_x2, m_y2, m_x3, m_y3, m_x4, m_y4; - QByteArray m_mask; -}; - -} - -#endif diff --git a/src/libshared/net/layer.cpp b/src/libshared/net/layer.cpp deleted file mode 100644 index 68d93d2c4a..0000000000 --- a/src/libshared/net/layer.cpp +++ /dev/null @@ -1,391 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -#include "libshared/net/layer.h" -#include "libshared/net/textmode.h" - -#include - -namespace protocol { - -CanvasResize *CanvasResize::deserialize(uint8_t ctx, const uchar *data, uint len) -{ - if(len!=16) - return nullptr; - return new CanvasResize( - ctx, - qFromBigEndian(data+0), - qFromBigEndian(data+4), - qFromBigEndian(data+8), - qFromBigEndian(data+12) - ); -} - -int CanvasResize::payloadLength() const -{ - return 4*4; -} - -int CanvasResize::serializePayload(uchar *data) const -{ - uchar *ptr = data; - qToBigEndian(m_top, ptr); ptr += 4; - qToBigEndian(m_right, ptr); ptr += 4; - qToBigEndian(m_bottom, ptr); ptr += 4; - qToBigEndian(m_left, ptr); ptr += 4; - return ptr - data; -} - -Kwargs CanvasResize::kwargs() const -{ - Kwargs kw; - if(m_top) - kw["top"] = QString::number(m_top); - if(m_right) - kw["right"] = QString::number(m_right); - if(m_bottom) - kw["bottom"] = QString::number(m_bottom); - if(m_left) - kw["left"] = QString::number(m_left); - return kw; -} - -CanvasResize *CanvasResize::fromText(uint8_t ctx, const Kwargs &kwargs) -{ - return new CanvasResize( - ctx, - kwargs["top"].toInt(), - kwargs["right"].toInt(), - kwargs["bottom"].toInt(), - kwargs["left"].toInt() - ); -} - -LayerCreate *LayerCreate::deserialize(uint8_t ctx, const uchar *data, uint len) -{ - if(len<9) - return nullptr; - - return new LayerCreate( - ctx, - qFromBigEndian(data+0), - qFromBigEndian(data+2), - qFromBigEndian(data+4), - *(data+8), - QByteArray(reinterpret_cast(data)+9, len-9) - ); -} - -int LayerCreate::payloadLength() const -{ - return 9 + m_title.length(); -} - -int LayerCreate::serializePayload(uchar *data) const -{ - uchar *ptr = data; - qToBigEndian(m_id, ptr); ptr += 2; - qToBigEndian(m_source, ptr); ptr += 2; - qToBigEndian(m_fill, ptr); ptr += 4; - *(ptr++) = m_flags; - memcpy(ptr, m_title.constData(), m_title.length()); - ptr += m_title.length(); - return ptr - data; -} - -Kwargs LayerCreate::kwargs() const -{ - Kwargs kw; - kw["id"] = text::idString(m_id); - if(m_source) - kw["source"] = text::idString(m_source); - if(m_fill) - kw["fill"] = text::argbString(m_fill); - QStringList flags; - if((m_flags&FLAG_COPY)) - flags << "copy"; - if((m_flags&FLAG_INSERT)) - flags << "insert"; - if(!flags.isEmpty()) - kw["flags"] = flags.join(','); - kw["title"] = title(); - return kw; -} - -LayerCreate *LayerCreate::fromText(uint8_t ctx, const Kwargs &kwargs) -{ - QStringList flags = kwargs["flags"].split(','); - return new LayerCreate( - ctx, - text::parseIdString16(kwargs["id"]), - text::parseIdString16(kwargs["source"]), - text::parseColor(kwargs["fill"]), - (flags.contains("copy") ? FLAG_COPY : 0) | - (flags.contains("insert") ? FLAG_INSERT : 0), - kwargs["title"] - ); -} - -LayerAttributes *LayerAttributes::deserialize(uint8_t ctx, const uchar *data, uint len) -{ - if(len!=6) - return nullptr; - return new LayerAttributes( - ctx, - qFromBigEndian(data+0), - *(data+2), - *(data+3), - *(data+4), - *(data+5) - ); -} - -int LayerAttributes::payloadLength() const -{ - return 6; -} - - -int LayerAttributes::serializePayload(uchar *data) const -{ - uchar *ptr=data; - qToBigEndian(m_id, ptr); ptr += 2; - *(ptr++) = m_sublayer; - *(ptr++) = m_flags; - *(ptr++) = m_opacity; - *(ptr++) = m_blend; - return ptr-data; -} - -Kwargs LayerAttributes::kwargs() const -{ - Kwargs kw; - kw["layer"] = text::idString(m_id); - if(m_sublayer>0) - kw["sublayer"] = QString::number(m_sublayer); - kw["opacity"] = text::decimal(m_opacity); - kw["blend"] = QString::number(m_blend); - - QStringList flags; - if(isCensored()) - flags << "censor"; - if(isFixed()) - flags << "fixed"; - if(!flags.isEmpty()) - kw["flags"] = flags.join(','); - - return kw; -} - -LayerAttributes *LayerAttributes::fromText(uint8_t ctx, const Kwargs &kwargs) -{ - QStringList flags = kwargs["flags"].split(','); - return new LayerAttributes( - ctx, - text::parseIdString16(kwargs["layer"]), - kwargs["sublayer"].toInt(), - (flags.contains("censor") ? FLAG_CENSOR : 0) | - (flags.contains("fixed") ? FLAG_FIXED : 0), - text::parseDecimal8(kwargs["opacity"]), - kwargs["blend"].toInt() - ); -} - -LayerVisibility *LayerVisibility::deserialize(uint8_t ctx, const uchar *data, uint len) -{ - if(len!=3) - return nullptr; - return new LayerVisibility( - ctx, - qFromBigEndian(data+0), - *(data+2) - ); -} - -int LayerVisibility::payloadLength() const -{ - return 3; -} - - -int LayerVisibility::serializePayload(uchar *data) const -{ - uchar *ptr=data; - qToBigEndian(m_id, ptr); ptr += 2; - *(ptr++) = m_visible; - return ptr-data; -} - -Kwargs LayerVisibility::kwargs() const -{ - Kwargs kw; - kw["id"] = text::idString(m_id); - kw["visible"] = m_visible ? "true" : "false"; - return kw; -} - -LayerVisibility *LayerVisibility::fromText(uint8_t ctx, const Kwargs &kwargs) -{ - return new LayerVisibility( - ctx, - text::parseIdString16(kwargs["id"]), - kwargs["visible"] == "true" - ); -} - -LayerRetitle *LayerRetitle::deserialize(uint8_t ctx, const uchar *data, uint len) -{ - if(len<2) - return nullptr; - return new LayerRetitle( - ctx, - qFromBigEndian(data+0), - QByteArray(reinterpret_cast(data)+2,len-2) - ); -} - -int LayerRetitle::payloadLength() const -{ - return 2 + m_title.length(); -} - - -int LayerRetitle::serializePayload(uchar *data) const -{ - uchar *ptr = data; - qToBigEndian(m_id, ptr); ptr += 2; - memcpy(ptr, m_title.constData(), m_title.length()); - ptr += m_title.length(); - return ptr - data; -} - -Kwargs LayerRetitle::kwargs() const -{ - Kwargs kw; - kw["id"] = text::idString(m_id); - kw["title"] = title(); - return kw; -} - -LayerRetitle *LayerRetitle::fromText(uint8_t ctx, const Kwargs &kwargs) -{ - return new LayerRetitle( - ctx, - text::parseIdString16(kwargs["id"]), - kwargs["title"] - ); -} - -LayerOrder *LayerOrder::deserialize(uint8_t ctx, const uchar *data, uint len) -{ - if((len%2) != 0) - return nullptr; - - QList order; - order.reserve(len / 2); - for(uint i=0;i(data+i)); - - return new LayerOrder(ctx, order); -} - -int LayerOrder::payloadLength() const -{ - return m_order.size() * 2; -} - -int LayerOrder::serializePayload(uchar *data) const -{ - uchar *ptr = data; - for(uint16_t l : m_order) { - qToBigEndian(l, ptr); ptr += 2; - } - return ptr - data; -} - -QList LayerOrder::sanitizedOrder(const QList ¤tOrder) const -{ - QList S; - S.reserve(currentOrder.size()); - - // remove duplicates and IDs not found in the current order - for(uint16_t l : m_order) { - if(!S.contains(l) && currentOrder.contains(l)) - S.append(l); - } - - // at this point, S contains no duplicate items and no items not in currentOrder - // therefore |S| <= |currentOrder| - - // add leftover IDs from currentOrder - int i=0; - while(S.size() < currentOrder.size()) { - if(!S.contains(currentOrder.at(i))) - S.append(currentOrder.at(i)); - ++i; - } - - // the above loop ends when |S| == |currentOrder|. Since S may not contain duplicates - // or items not in currentOrder, S must be a permutation of currentOrder. - - return S; -} - -Kwargs LayerOrder::kwargs() const -{ - Kwargs kw; - kw["layers"] = text::idListString(m_order); - return kw; -} - -LayerOrder *LayerOrder::fromText(uint8_t ctx, const Kwargs &kwargs) -{ - return new LayerOrder( - ctx, - text::parseIdListString16(kwargs["layers"]) - ); -} - -LayerDelete *LayerDelete::deserialize(uint8_t ctx, const uchar *data, uint len) -{ - if(len != 3) - return nullptr; - return new LayerDelete( - ctx, - qFromBigEndian(data+0), - data[2] - ); -} - -int LayerDelete::payloadLength() const -{ - return 2 + 1; -} - -int LayerDelete::serializePayload(uchar *data) const -{ - uchar *ptr = data; - qToBigEndian(m_id, ptr); ptr += 2; - *(ptr++) = m_merge; - return ptr - data; -} - -Kwargs LayerDelete::kwargs() const -{ - Kwargs kw; - kw["id"] = text::idString(m_id); - if(m_merge) - kw["merge"] = "true"; - return kw; -} - -LayerDelete *LayerDelete::fromText(uint8_t ctx, const Kwargs &kwargs) -{ - return new LayerDelete( - ctx, - text::parseIdString16(kwargs["id"]), - kwargs["merge"] == "true" - ); -} - -} - diff --git a/src/libshared/net/layer.h b/src/libshared/net/layer.h deleted file mode 100644 index b4e5f8d51a..0000000000 --- a/src/libshared/net/layer.h +++ /dev/null @@ -1,326 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -#ifndef DP_NET_LAYER_H -#define DP_NET_LAYER_H - -#include -#include -#include - -#include "libshared/net/message.h" - -namespace protocol { - -/** - * \brief Canvas size adjustment command - * - * This is the first command that must be sent to initialize the session. - * - * This affects the size of all existing and future layers. - * - * The new canvas size is relative to the old one. The four adjustement - * parameters extend or rectract their respective borders. - * Initial canvas resize should be (0, w, h, 0). - */ -class CanvasResize final : public Message { -public: - CanvasResize(uint8_t ctx, int32_t top, int32_t right, int32_t bottom, int32_t left) - : Message(MSG_CANVAS_RESIZE, ctx), m_top(top), m_right(right), m_bottom(bottom), m_left(left) - {} - - static CanvasResize *deserialize(uint8_t ctx, const uchar *data, uint len); - static CanvasResize *fromText(uint8_t ctx, const Kwargs &kwargs); - - int32_t top() const { return m_top; } - int32_t right() const { return m_right; } - int32_t bottom() const { return m_bottom; } - int32_t left() const { return m_left; } - - QString messageName() const override { return QStringLiteral("resize"); } - -protected: - int payloadLength() const override; - int serializePayload(uchar *data) const override; - Kwargs kwargs() const override; - -private: - int32_t m_top; - int32_t m_right; - int32_t m_bottom; - int32_t m_left; -}; - -/** - * \brief Layer creation command. - * - * A session starts with zero layers, so a layer creation command is typically - * the second command to be sent, right after setting the canvas size. - * - * The layer ID must be prefixed with the context ID of the user creating it. - * This allows users to choose the layer ID themselves without worrying about - * clashes. In single user mode, the client can assign IDs as it pleases, - * but in multiuser mode the server validates the prefix for all new layers. - * - * The following flags can be used with layer creation: - * - COPY -- a copy of the Source layer is made, rather than a blank layer - * - INSERT -- the new layer is inserted above the Source layer. Source 0 means - * the layer will be placed bottom-most on the stack - * - * The Source layer ID should be zero when COPY or INSERT flags are not used. - * When COPY is used, it should refer to an existing layer. Copy commands - * referring to missing layers are dropped. - * When INSERT is used, referring to 0 or a nonexistent layer places - * the new layer at the bottom of the stack. - * - * If layer controls are locked, this command requires session operator privileges. - */ -class LayerCreate final : public Message { -public: - static const uint8_t FLAG_COPY = 0x01; - static const uint8_t FLAG_INSERT = 0x02; - - LayerCreate(uint8_t ctxid, uint16_t id, uint16_t source, uint32_t fill, uint8_t flags, const QString &title) - : Message(MSG_LAYER_CREATE, ctxid), m_id(id), m_source(source), m_fill(fill), m_flags(flags), m_title(title.toUtf8()) - {} - - static LayerCreate *deserialize(uint8_t ctx, const uchar *data, uint len); - static LayerCreate *fromText(uint8_t ctx, const Kwargs &kwargs); - - uint16_t layer() const override { return m_id; } - uint16_t source() const { return m_source; } - uint32_t fill() const { return m_fill; } - uint8_t flags() const { return m_flags; } - QString title() const { return QString::fromUtf8(m_title); } - - /** - * @brief Check if the ID's namespace portition matches the context ID - * - * Note. This check is only needed during normal multiuser operation. Layers - * created in single-user mode can use any ID. - * This means layer IDs of the initial snapshot need not be validated. - */ - bool isValidId() const { return (m_id>>8) == contextId(); } - - QString messageName() const override { return QStringLiteral("newlayer"); } - -protected: - int payloadLength() const override; - int serializePayload(uchar *data) const override; - Kwargs kwargs() const override; - -private: - uint16_t m_id; - uint16_t m_source; - uint32_t m_fill; - uint8_t m_flags; - QByteArray m_title; -}; - -/** - * @brief Layer attribute change command - * - * If the current layer or layer controls in general are locked, this command - * requires session operator privileges. - * - * Specifying a sublayer requires session operator privileges. Currently, it is used - * only when sublayers are needed at canvas initialization. - */ -class LayerAttributes final : public Message { -public: - static const uint8_t FLAG_CENSOR = 0x01; // censored layer - static const uint8_t FLAG_FIXED = 0x02; // fixed background/foreground layer (drawn even in solo modo) - - LayerAttributes(uint8_t ctx, uint16_t id, uint8_t sublayer, uint8_t flags, uint8_t opacity, uint8_t blend) - : Message(MSG_LAYER_ATTR, ctx), m_id(id), - m_sublayer(sublayer), m_flags(flags), m_opacity(opacity), m_blend(blend) - {} - - static LayerAttributes *deserialize(uint8_t ctx, const uchar *data, uint len); - static LayerAttributes *fromText(uint8_t ctx, const Kwargs &kwargs); - - uint16_t layer() const override { return m_id; } - uint8_t sublayer() const { return m_sublayer; } - uint8_t flags() const { return m_flags; } - uint8_t opacity() const { return m_opacity; } - uint8_t blend() const { return m_blend; } - - bool isCensored() const { return m_flags & FLAG_CENSOR; } - bool isFixed() const { return m_flags & FLAG_FIXED; } - - QString messageName() const override { return QStringLiteral("layerattr"); } - -protected: - int payloadLength() const override; - int serializePayload(uchar *data) const override; - Kwargs kwargs() const override; - -private: - uint16_t m_id; - uint8_t m_sublayer; - uint8_t m_flags; - uint8_t m_opacity; - uint8_t m_blend; -}; - -/** - * @brief Layer visibility (visible/hidden) change command - * - * This command is used to toggle the layer visibility for the local user. - * (I.e. any user is allowed to send this command and it has no effect on - * other users.) - * Even though this only affects the sending user, this message can be - * sent through the official session history to keep the architecture simple. - * - * Note: to hide the layer for all users, use LayerAttributes to set its opacity - * to zero. - */ -class LayerVisibility final : public Message { -public: - LayerVisibility(uint8_t ctx, uint16_t id, uint8_t visible) - : Message(MSG_LAYER_VISIBILITY, ctx), m_id(id), m_visible(visible) - { } - - static LayerVisibility *deserialize(uint8_t ctx, const uchar *data, uint len); - static LayerVisibility *fromText(uint8_t ctx, const Kwargs &kwargs); - - uint16_t layer() const override { return m_id; } - uint8_t visible() const { return m_visible; } - - QString messageName() const override { return QStringLiteral("layervisibility"); } - -protected: - int payloadLength() const override; - int serializePayload(uchar *data) const override; - Kwargs kwargs() const override; - -private: - uint16_t m_id; - uint8_t m_visible; -}; - -/** - * @brief Layer title change command - * - * If the current layer or layer controls in general are locked, this command - * requires session operator privileges. - */ -class LayerRetitle final : public Message { -public: - LayerRetitle(uint8_t ctx, uint16_t id, const QByteArray &title) - : Message(MSG_LAYER_RETITLE, ctx), m_id(id), m_title(title) - {} - LayerRetitle(uint8_t ctx, uint16_t id, const QString &title) - : LayerRetitle(ctx, id, title.toUtf8()) - {} - - static LayerRetitle *deserialize(uint8_t ctx, const uchar *data, uint len); - static LayerRetitle *fromText(uint8_t ctx, const Kwargs &kwargs); - - uint16_t layer() const override { return m_id; } - QString title() const { return QString::fromUtf8(m_title); } - - QString messageName() const override { return QStringLiteral("retitlelayer"); } - -protected: - int payloadLength() const override; - int serializePayload(uchar *data) const override; - Kwargs kwargs() const override; - -private: - uint16_t m_id; - QByteArray m_title; -}; - -/** - * @brief Layer order change command - * - * New layers are always added to the top of the stack. - * This command includes a list of layer IDs that define the new stacking order. - * - * An order change should list all layers in the stack, but due to synchronization issues, that - * is not always possible. - * The layer order should therefore be sanitized by removing all layers not in the current layer stack - * and adding all missing layers to the end in their current relative order. - * - * For example: if the current stack is [1,2,3,4,5] and the client receives - * a reordering command [3,4,1], the missing layers are appended: [3,4,1,2,5]. - * - * If layer controls are locked, this command requires session operator privileges. - */ -class LayerOrder final : public Message { -public: - LayerOrder(uint8_t ctx, const QList &order) - : Message(MSG_LAYER_ORDER, ctx), - m_order(order) - {} - - static LayerOrder *deserialize(uint8_t ctx, const uchar *data, uint len); - static LayerOrder *fromText(uint8_t ctx, const Kwargs &kwargs); - - const QList &order() const { return m_order; } - - /** - * @brief Get sanitized layer order - * - * This function checks that the new ordering is valid in respect to the current order - * and returns a sanitized ordering. - * - * The following corrections are made: - * - duplicate IDs are removed - * - IDs not in the current order are removed - * - missing IDs are appended to the new order - * - * @param currentOrder the current ordering - * @return cleaned up ordering - */ - QList sanitizedOrder(const QList ¤tOrder) const; - - QString messageName() const override { return QStringLiteral("layerorder"); } -protected: - int payloadLength() const override; - int serializePayload(uchar *data) const override; - Kwargs kwargs() const override; - -private: - QList m_order; -}; - -/** - * @brief Layer deletion command - * - * If the merge attribute is set, the contents of the layer is merged - * to the layer below it. Merging the bottom-most layer does nothing. - * - * If the current layer or layer controls in general are locked, this command - * requires session operator privileges. - */ -class LayerDelete final : public Message { -public: - LayerDelete(uint8_t ctx, uint16_t id, uint8_t merge) - : Message(MSG_LAYER_DELETE, ctx), - m_id(id), - m_merge(merge) - {} - - static LayerDelete *deserialize(uint8_t ctx, const uchar *data, uint len); - static LayerDelete *fromText(uint8_t ctx, const Kwargs &kwargs); - - uint16_t layer() const override { return m_id; } - uint8_t merge() const { return m_merge; } - - QString messageName() const override { return QStringLiteral("deletelayer"); } - -protected: - int payloadLength() const override; - int serializePayload(uchar *data) const override; - Kwargs kwargs() const override; - -private: - uint16_t m_id; - uint8_t m_merge; -}; - -} - -#endif - diff --git a/src/libshared/net/message.cpp b/src/libshared/net/message.cpp index 5269d6d947..4e455107d1 100644 --- a/src/libshared/net/message.cpp +++ b/src/libshared/net/message.cpp @@ -1,171 +1,368 @@ // SPDX-License-Identifier: GPL-3.0-or-later - #include "libshared/net/message.h" -#include "libshared/net/control.h" -#include "libshared/net/meta.h" -#include "libshared/net/opaque.h" -#include "libshared/net/recording.h" +#include "libshared/util/qtcompat.h" +#include +#include +#include + +namespace net { -#include -#include -#include +Message Message::null() +{ + return Message{nullptr}; +} -namespace protocol { +Message Message::inc(DP_Message *msg) +{ + return Message{DP_message_incref_nullable(msg)}; +} -int Message::sniffLength(const char *data) +Message Message::noinc(DP_Message *msg) { - // extract payload length - quint16 len = qFromBigEndian(reinterpret_cast(data)); + return Message{msg}; +} - // return total message length - return len + HEADER_LEN; +Message Message::deserialize( + const unsigned char *buf, size_t bufsize, bool decodeOpaque) +{ + return Message::noinc(DP_message_deserialize(buf, bufsize, decodeOpaque)); } -int Message::serialize(char *data) const + +DP_Message **Message::asRawMessages(const net::Message *msgs) { - // Fixed header: payload length + message type + context ID - qToBigEndian(quint16(payloadLength()), reinterpret_cast(data)); data += 2; - *(data++) = m_type; - *(data++) = m_contextid; + // We want to do a moderately evil reinterpret cast of a drawdance::Message + // to its underlying pointer. Let's make sure that it's a valid thing to do. + // Make sure it's a standard layout class, because only for those it's legal + // to cast them to their first member. + static_assert( + std::is_standard_layout::value, + "drawdance::Message is standard layout for reinterpretation to " + "DP_Message"); + // And then ensure that there's only the pointer member. + static_assert( + sizeof(net::Message) == sizeof(DP_Message *), + "drawdance::Message has the same size as a DP_Message pointer"); + // Alright, that means this cast, despite looking terrifying, is legal. The + // const can be cast away safely too because the underlying pointer isn't. + return reinterpret_cast(const_cast(msgs)); +} - // Message payload. (May be 0 length) - int written = serializePayload(reinterpret_cast(data)); - Q_ASSERT(written == payloadLength()); - Q_ASSERT(written <= 0xffff); - return HEADER_LEN + written; +Message::Message() + : Message(nullptr) +{ } -bool Message::equals(const Message &m) const +Message::Message(const Message &other) + : Message{DP_message_incref_nullable(other.m_data)} { - if(type() != m.type() || contextId() != m.contextId()) - return false; +} + +Message::Message(Message &&other) + : Message{other.m_data} +{ + other.m_data = nullptr; +} + +Message &Message::operator=(const Message &other) +{ + DP_message_decref_nullable(m_data); + m_data = DP_message_incref_nullable(other.m_data); + return *this; +} + +Message &Message::operator=(Message &&other) +{ + DP_message_decref_nullable(m_data); + m_data = other.m_data; + other.m_data = nullptr; + return *this; +} + +Message::~Message() +{ + DP_message_decref_nullable(m_data); +} + +DP_Message *Message::get() const +{ + return m_data; +} + +bool Message::isNull() const +{ + return !m_data; +} + +DP_MessageType Message::type() const +{ + return DP_message_type(m_data); +} + +QString Message::typeName() const +{ + return QString::fromUtf8(DP_message_type_name(type())); +} + +bool Message::isControl() const +{ + return DP_message_type_control(type()); +} + +bool Message::isInCommandRange() const +{ + return type() >= 128; +} + +unsigned int Message::contextId() const +{ + return DP_message_context_id(m_data); +} + +void Message::setContextId(unsigned int contextId) +{ + DP_message_context_id_set(m_data, contextId); +} - return payloadEquals(m); +void Message::setIndirectCompatFlag() +{ + DP_message_compat_flag_indirect_set(m_data); } -bool Message::payloadEquals(const Message &m) const +size_t Message::length() const { -#if 0 - qDebug("default inefficient Message::payloadEquals called (type=%d).", type()); -#endif + return DP_message_length(m_data); +} - if(payloadLength() != m.payloadLength()) +bool Message::equals(const Message &other) const +{ + if(m_data == other.m_data) { + return true; + } else if(m_data && other.m_data) { + return DP_message_equals(m_data, other.m_data); + } else { return false; + } +} - QByteArray b1(payloadLength(), 0); - QByteArray b2(payloadLength(), 0); - serializePayload(reinterpret_cast(b1.data())); - m.serializePayload(reinterpret_cast(b2.data())); +DP_MsgChat *Message::toChat() const +{ + Q_ASSERT(type() == DP_MSG_CHAT); + return static_cast(DP_message_internal(m_data)); +} - return b1 == b2; +DP_MsgData *Message::toData() const +{ + Q_ASSERT(type() == DP_MSG_DATA); + return static_cast(DP_message_internal(m_data)); } -NullableMessageRef Message::deserialize(const uchar *data, int buflen, bool decodeOpaque) +DP_MsgDrawDabsClassic *Message::toDrawDabsClassic() const { - // All valid messages have the fixed length header - if(buflen(DP_message_internal(m_data)); +} - const quint16 len = qFromBigEndian(data); +DP_MsgDrawDabsPixel *Message::toDrawDabsPixel() const +{ + Q_ASSERT( + type() == DP_MSG_DRAW_DABS_PIXEL || + type() == DP_MSG_DRAW_DABS_PIXEL_SQUARE); + return static_cast(DP_message_internal(m_data)); +} - if(buflen < len+HEADER_LEN) - return nullptr; +DP_MsgFeatureAccessLevels *Message::toFeatureAccessLevels() const +{ + Q_ASSERT(type() == DP_MSG_FEATURE_ACCESS_LEVELS); + return static_cast( + DP_message_internal(m_data)); +} - const MessageType type = MessageType(data[2]); - const uint8_t ctx = data[3]; +DP_MsgFillRect *Message::toFillRect() const +{ + Q_ASSERT(type() == DP_MSG_FILL_RECT); + return static_cast(DP_message_internal(m_data)); +} - data += HEADER_LEN; +DP_MsgLayerAttributes *Message::toLayerAttributes() const +{ + Q_ASSERT(type() == DP_MSG_LAYER_ATTRIBUTES); + return static_cast(DP_message_internal(m_data)); +} + +DP_MsgLayerTreeCreate *Message::toLayerTreeCreate() const +{ + Q_ASSERT(type() == DP_MSG_LAYER_TREE_CREATE); + return static_cast(DP_message_internal(m_data)); +} + +DP_MsgPrivateChat *Message::toPrivateChat() const +{ + Q_ASSERT(type() == DP_MSG_PRIVATE_CHAT); + return static_cast(DP_message_internal(m_data)); +} + +DP_MsgPutImage *Message::toPutImage() const +{ + Q_ASSERT(type() == DP_MSG_PUT_IMAGE); + return static_cast(DP_message_internal(m_data)); +} + +DP_MsgServerCommand *Message::toServerCommand() const +{ + Q_ASSERT(type() == DP_MSG_SERVER_COMMAND); + return static_cast(DP_message_internal(m_data)); +} + +DP_MsgSessionOwner *Message::toSessionOwner() const +{ + Q_ASSERT(type() == DP_MSG_SESSION_OWNER); + return static_cast(DP_message_internal(m_data)); +} + +DP_MsgTrustedUsers *Message::toTrustedUsers() const +{ + Q_ASSERT(type() == DP_MSG_TRUSTED_USERS); + return static_cast(DP_message_internal(m_data)); +} - Message *msg = nullptr; +DP_MsgUserAcl *Message::toUserAcl() const +{ + Q_ASSERT(type() == DP_MSG_USER_ACL); + return static_cast(DP_message_internal(m_data)); +} - switch(type) { - // Control messages - case MSG_COMMAND: msg = Command::deserialize(ctx, data, len); break; - case MSG_DISCONNECT: msg = Disconnect::deserialize(ctx, data, len); break; - case MSG_PING: msg = Ping::deserialize(ctx, data, len); break; - case MSG_INTERNAL: - qWarning("Tried to deserialize MSG_INTERVAL"); - return NullableMessageRef(); - // Transparent meta messages - case MSG_USER_JOIN: msg = UserJoin::deserialize(ctx, data, len); break; - case MSG_USER_LEAVE: msg = UserLeave::deserialize(ctx, data, len); break; - case MSG_SESSION_OWNER: msg = SessionOwner::deserialize(ctx, data, len); break; - case MSG_CHAT: msg = Chat::deserialize(ctx, data, len); break; - case MSG_TRUSTED_USERS: msg = TrustedUsers::deserialize(ctx, data, len); break; - case MSG_SOFTRESET: msg = SoftResetPoint::deserialize(ctx, data, len); break; - case MSG_PRIVATE_CHAT: msg = PrivateChat::deserialize(ctx, data, len); break; +bool Message::serialize(QByteArray &buffer) const +{ + return DP_message_serialize(m_data, true, getDeserializeBuffer, &buffer) != + 0; +} - // Opaque messages +bool Message::shouldSmoothe() const +{ + switch(type()) { + case DP_MSG_DRAW_DABS_CLASSIC: + case DP_MSG_DRAW_DABS_PIXEL: + case DP_MSG_DRAW_DABS_PIXEL_SQUARE: + case DP_MSG_DRAW_DABS_MYPAINT: + case DP_MSG_MOVE_POINTER: + return true; default: - if(type >= 64) { - if(decodeOpaque) - return OpaqueMessage::decode(type, ctx, data, len); - else - msg = new OpaqueMessage(type, ctx, data, len); - } + return false; } +} - if(!msg) - qWarning("Unhandled message type %d", type); - return NullableMessageRef(msg); -} - -QString Message::toString() const -{ - const Kwargs kw = kwargs(); - QString str = QStringLiteral("%1 %2").arg(contextId()).arg(messageName()); - - // Add non-multiline keyword args - const QRegularExpression space("\\s"); - bool hasMultiline = false; - { - KwargsIterator i(kw); - while(i.hasNext()) { - i.next(); - if(i.value().contains(space)) { - hasMultiline = true; - } else { - str = str + ' ' + i.key() + '=' + i.value(); - } - } +void Message::setUchars(size_t size, unsigned char *out, void *user) +{ + if(size > 0) { + memcpy(out, user, size); } +} - // Add multiline keyword args - if(hasMultiline) { - str += " {"; - KwargsIterator i(kw); - i.toFront(); - do { - i.next(); - if(i.value().contains(space)) { - QStringList lines = i.value().split('\n'); - for(const QString &line : lines) { - str += "\n\t"; - str += i.key(); - str += "="; - str += line; - } - } - } while(i.hasNext()); - str += "\n}"; +void Message::setUint8s(int count, uint8_t *out, void *user) +{ + if(count > 0) { + memcpy(out, user, sizeof(uint8_t) * count); } - return str; } -MessagePtr Message::asFiltered() const +void Message::setUint16s(int count, uint16_t *out, void *user) { - Q_ASSERT(type() != MSG_FILTERED); // no nested wrappings please - int len = 1 + payloadLength(); - uchar *payload = new uchar[len]; + if(count > 0) { + memcpy(out, user, sizeof(uint16_t) * count); + } +} - payload[0] = type(); - serializePayload(payload+1); +Message::Message(DP_Message *msg) + : m_data{msg} +{ +} + +unsigned char *Message::getDeserializeBuffer(void *user, size_t size) +{ + QByteArray *buffer = static_cast(user); + buffer->resize(compat::castSize(size)); + return reinterpret_cast(buffer->data()); +} + + +Message makeChatMessage( + uint8_t contextId, uint8_t tflags, uint8_t oflags, const QString &message) +{ + QByteArray bytes = message.toUtf8(); + return Message::noinc(DP_msg_chat_new( + contextId, tflags, oflags, bytes.constData(), bytes.length())); +} + +Message +makeDisconnectMessage(uint8_t contextId, uint8_t reason, const QString &message) +{ + QByteArray bytes = message.toUtf8(); + return Message::noinc(DP_msg_disconnect_new( + contextId, reason, bytes.constData(), bytes.length())); +} - return MessagePtr(new Filtered(contextId(), payload, qMin(len, 0xffff))); +Message makeJoinMessage( + uint8_t contextId, uint8_t flags, const QString &name, + const QByteArray &avatar) +{ + QByteArray nameBytes = name.toUtf8(); + return Message::noinc(DP_msg_join_new( + contextId, flags, nameBytes.constData(), nameBytes.size(), + Message::setUchars, avatar.size(), + const_cast(avatar.constData()))); } +Message makeLeaveMessage(uint8_t contextId) +{ + return Message::noinc(DP_msg_leave_new(contextId)); } +Message makePingMessage(uint8_t contextId, bool isPong) +{ + return Message::noinc(DP_msg_ping_new(contextId, isPong)); +} + +Message makePrivateChatMessage( + uint8_t contextId, uint8_t target, uint8_t oflags, const QString &message) +{ + QByteArray bytes = message.toUtf8(); + return Message::noinc(DP_msg_private_chat_new( + contextId, target, oflags, bytes.constData(), bytes.length())); +} + +Message makeServerCommandMessage(uint8_t contextId, const QJsonDocument &msg) +{ + QByteArray msgBytes = msg.toJson(QJsonDocument::Compact); + if(msgBytes.length() <= + DP_MESSAGE_MAX_PAYLOAD_LENGTH - DP_MSG_SERVER_COMMAND_STATIC_LENGTH) { + return Message::noinc(DP_msg_server_command_new( + contextId, msgBytes.constData(), msgBytes.length())); + } else { + qWarning( + "ServerCommand too long (%lld bytes)", + compat::cast(msgBytes.length())); + return Message::null(); + } +} + +Message +makeSessionOwnerMessage(uint8_t contextId, const QVector &users) +{ + return Message::noinc(DP_msg_session_owner_new( + contextId, Message::setUint8s, users.count(), + const_cast(users.constData()))); +} + +Message +makeTrustedUsersMessage(uint8_t contextId, const QVector &users) +{ + return Message::noinc(DP_msg_trusted_users_new( + contextId, Message::setUint8s, users.count(), + const_cast(users.constData()))); +} + +} diff --git a/src/libshared/net/message.h b/src/libshared/net/message.h index 4acb95c305..f414ccff16 100644 --- a/src/libshared/net/message.h +++ b/src/libshared/net/message.h @@ -1,506 +1,114 @@ // SPDX-License-Identifier: GPL-3.0-or-later +#ifndef LIBSHARED_DRAWDANCE_MESSAGE_H +#define LIBSHARED_DRAWDANCE_MESSAGE_H +extern "C" { +#include +} +#include -#ifndef DP_NET_MESSAGE_H -#define DP_NET_MESSAGE_H - -#include -#include -#include -#include - -namespace protocol { - -/** - * Drawpile network protocol message types - */ -enum MessageType { - // Control messages (transparent) - MSG_COMMAND=0, - MSG_DISCONNECT, - MSG_PING, - - // Reserved ID for internal use (not serializable) - MSG_INTERNAL=31, - - // Meta messages (transparent) - MSG_USER_JOIN=32, - MSG_USER_LEAVE, - MSG_SESSION_OWNER, - MSG_CHAT, - MSG_TRUSTED_USERS, - MSG_SOFTRESET, - MSG_PRIVATE_CHAT, - - // Meta messages (opaque) - MSG_INTERVAL=64, - MSG_LASERTRAIL, - MSG_MOVEPOINTER, - MSG_MARKER, - MSG_USER_ACL, - MSG_LAYER_ACL, - MSG_FEATURE_LEVELS, - MSG_LAYER_DEFAULT, - MSG_FILTERED, - MSG_EXTENSION, // reserved for non-standard extension use - - // Command messages (opaque) - MSG_UNDOPOINT=128, - MSG_CANVAS_RESIZE, - MSG_LAYER_CREATE, - MSG_LAYER_ATTR, - MSG_LAYER_RETITLE, - MSG_LAYER_ORDER, - MSG_LAYER_DELETE, - MSG_LAYER_VISIBILITY, - MSG_PUTIMAGE, - MSG_FILLRECT, - MSG_TOOLCHANGE_REMOVED, // replaced by drawdabs* - MSG_PEN_MOVE_REMOVED, // replaced by drawdabs* - MSG_PEN_UP, - MSG_ANNOTATION_CREATE, - MSG_ANNOTATION_RESHAPE, - MSG_ANNOTATION_EDIT, - MSG_ANNOTATION_DELETE, - MSG_REGION_MOVE, - MSG_PUTTILE, - MSG_CANVAS_BACKGROUND, - MSG_DRAWDABS_CLASSIC, - MSG_DRAWDABS_PIXEL, - MSG_DRAWDABS_PIXEL_SQUARE, - MSG_UNDO=255, -}; - -enum MessageUndoState { - DONE = 0x00, /* done/not undone */ - UNDONE = 0x01, /* marked as undone, can be redone */ - GONE = 0x03 /* marked as undone, cannot be redone */ -}; +class QByteArray; +class QJsonDocument; +class QString; +struct DP_Message; -// Note: both QHash and QMap work here, but we use QMap so the kwargs are -// ordered consistently, which makes comparing messages by eye easier. -typedef QMap Kwargs; -typedef QMapIterator KwargsIterator; +namespace net { -class MessagePtr; -class NullableMessageRef; +using MessageList = QVector; -class Message { - friend class MessagePtr; - friend class NullableMessageRef; +class Message final { public: - //! Length of the fixed message header - static const int HEADER_LEN = 4; - - Message(MessageType type, uint8_t ctx): m_type(type), _undone(DONE), m_refcount(0), m_contextid(ctx) {} - Message(Message &other) = delete; - Message(Message &&other) = delete; - Message &operator=(Message &other) = delete; - Message &operator=(Message &&other) = delete; - virtual ~Message() = default; - - /** - * @brief Get the type of this message. - * @return message type - */ - MessageType type() const { return m_type; } - - /** - * @brief Is this a control message - * - * Control messages are used for things that are related to the server and not - * the session directly (e.g. setting server settings.) - */ - bool isControl() const { return m_type < 32; } - - /** - * @brief Is this a meta message? - * - * Meta messages are part of the session, but do not directly affect drawing. - * However, some meta message (those related to access controls) do affect - * how the command messages are filtered. - */ - bool isMeta() const { return m_type >= 31 && m_type < 128; } - - /** - * @brief Check if this message type is a command stream type - * - * Command stream messages are the messages directly related to drawing. - * The canvas can be reconstructed exactly using only command messages. - * @return true if this is a drawing command - */ - bool isCommand() const { return m_type >= 128; } - - /** - * @brief Is this an opaque message - * - * Opaque messages are those messages that the server does not need to understand and can - * merely pass along as binary data. - */ - bool isOpaque() const { return m_type >= 64; } - - /** - * @brief Is this a recordable message? - * - * All Meta and Command messages are recordable. Only the Control messages, - * which are used just for client/server communications, are ignored. - */ - bool isRecordable() const { return m_type >= 32; } - - /** - * @brief Get the message length, header included - * @return message length in bytes - */ - int length() const { return HEADER_LEN + payloadLength(); } - - /** - * @brief Get the user context ID of this message - * - * The ID is 0 for messages that are not related to any user - * @return context ID or 0 if not applicable - */ - uint8_t contextId() const { return m_contextid; } - - /** - * @brief Set the user ID of this message - * - * @param userid the new user id - */ - void setContextId(uint8_t userid) { m_contextid = userid; } - - /** - * @brief Get the ID of the layer this command affects - * - * For commands that do not affect any particular layer, 0 should - * be returned. - * - * Annotation editing commands can return the annotation ID here. - * - * @return layer (or equivalent) ID or 0 if not applicable - */ - virtual uint16_t layer() const { return 0; } - - /** - * @brief Is this message type undoable? - * - * By default, all Command messages are undoable. - * - * @return true if this action can be undone - */ - virtual bool isUndoable() const { return isCommand(); } - - /** - * @brief Has this command been marked as undone? - * - * Note. This is a purely local flag that is not part of the - * protocol. It is here to avoid the need to maintain an - * external undone action list. - * - * @return true if this message has been marked as undone - */ - MessageUndoState undoState() const { return _undone; } - - /** - * @brief Mark this message as undone - * - * Note. Not all messages are undoable. This function - * does nothing if this message type doesn't support undoing. - * - * @param undone new undo flag state - */ - void setUndoState(MessageUndoState undo) { if(isUndoable()) _undone = undo; } - - /** - * @brief Serialize this message - * - * The data buffer must be long enough to hold length() bytes. - * @param data buffer where to write the message - * @return number of bytes written (should always be length()) - */ - int serialize(char *data) const; - - /** - * @brief get the length of the message from the given data - * - * Data buffer should be at least two bytes long - * @param data data buffer - * @return length - */ - static int sniffLength(const char *data); - - /** - * @brief deserialize a message from data buffer - * - * The provided buffer should contain at least sniffLength(data) - * bytes. The parameter buflen is the maximum length of the buffer. - * If the announced length of the message is less than the buffer - * length, a null pointer is returned. - * - * If the message type is unrecognized or the message content is - * determined to be invalid, a null pointer is returned. - * - * @param data input data buffer - * @param buflen length of the data buffer - * @param decodeOpaque automatically decode opaque messages rather than returning OpaqueMessage - * @return message or 0 if type is unknown - */ - static NullableMessageRef deserialize(const uchar *data, int buflen, bool decodeOpaque); - - /** - * @brief Check if this message has the same content as the other one - * @param m - * @return - */ - bool equals(const Message &m) const; - - /** - * @brief Get the textmode serialization of this message - */ - virtual QString toString() const; - - //! Get the name of this message - virtual QString messageName() const = 0; - - /** - * @brief Get a copy of this message wrapped in a Filtered message - * - * This is used when a message is filtered away, but we want to preserve - * the message for debugging reasons. - * - * @return a new Filtered instance - */ - MessagePtr asFiltered() const; - -protected: - /** - * @brief Get the length of the message payload - * @return payload length in bytes - */ - virtual int payloadLength() const = 0; - - /** - * @brief Serialize the message payload - * @param data data buffer - * @return number of bytes written (should always be the same as payloadLenth()) - */ - virtual int serializePayload(uchar *data) const = 0; - - /** - * @brief Check if the other message has identical payload - * - * The default implementation calls serializePayload and does a bytewise comparison - * on that. Subclasses should override this with a more efficient check. - * - * @param m - * @return true if payloads are equal - */ - virtual bool payloadEquals(const Message &m) const; - - /** - * @brief Get the keyword arguments that describe this message - * - * This is used by toString() to generate the textmode serialization. - */ - virtual Kwargs kwargs() const = 0; + static Message null(); + static Message inc(DP_Message *cs); + static Message noinc(DP_Message *cs); + static Message + deserialize(const unsigned char *buf, size_t bufsize, bool decodeOpaque); -private: - const MessageType m_type; - MessageUndoState _undone; - int m_refcount; - uint8_t m_contextid; -}; + static DP_Message **asRawMessages(const Message *msgs); -// https://gcc.gnu.org/bugzilla/show_bug.cgi?id=69210 -namespace diagnostic_marker_private { - class [[maybe_unused]] AbstractMessageMarker : Message { - inline QString toString() const override { return QString(); } - }; -} + Message(); + Message(const Message &other); + Message(Message &&other); -typedef QList MessageList; + Message &operator=(const Message &other); + Message &operator=(Message &&other); -/** - * @brief Base class for messages without a payload - */ -template class ZeroLengthMessage : public Message { -public: - ZeroLengthMessage(MessageType type, uint8_t ctx) : Message(type, ctx) { } - - static M *deserialize(uint8_t ctx, const uchar *data, int buflen) { - Q_UNUSED(data); - if(buflen!=0) - return nullptr; - return new M(ctx); - } - - static M *fromText(uint8_t ctx, const Kwargs &) { - return new M(ctx); - } - -protected: - int payloadLength() const override { return 0; } - int serializePayload(uchar *data) const override { Q_UNUSED(data); return 0; } - bool payloadEquals(const Message &m) const override { Q_UNUSED(m); return true; } - Kwargs kwargs() const override { return Kwargs(); } -}; + ~Message(); -/** -* @brief A reference counting pointer for Messages -* -* This object is the length of a normal pointer so it can be used -* efficiently with QList. -* -* @todo use QAtomicInt if thread safety is needed -*/ -class MessagePtr { -public: - /** - * @brief Take ownership of the given raw Message pointer. - * - * The message will be deleted when reference count falls to zero. - * Null pointers are not allowed. - * @param msg - */ - explicit MessagePtr(Message *msg) - : d(msg) - { - Q_ASSERT(d); - Q_ASSERT(d->m_refcount==0); - ++d->m_refcount; - } - - MessagePtr(const MessagePtr &ptr) : d(ptr.d) { ++d->m_refcount; } - - static MessagePtr fromNullable(const NullableMessageRef &ref) { return MessagePtr(ref); } - - ~MessagePtr() - { - Q_ASSERT(d->m_refcount>0); - if(--d->m_refcount == 0) - delete d; - } - - MessagePtr &operator=(const MessagePtr &msg) - { - if(msg.d != d) { - Q_ASSERT(d->m_refcount>0); - if(--d->m_refcount == 0) - delete d; - d = msg.d; - ++d->m_refcount; - } - return *this; - } - - Message &operator*() const { return *d; } - Message *operator->() const { return d; } - - template msgtype &cast() const { return static_cast(*d); } - - inline bool equals(const MessagePtr &m) const { return d->equals(*m); } - inline bool equals(const NullableMessageRef &m) const; + DP_Message *get() const; -private: - inline MessagePtr(const NullableMessageRef &ref); + bool isNull() const; - Message *d; -}; + DP_MessageType type() const; + QString typeName() const; -/** -* @brief A nullable reference counting pointer for Messages -* -* This object is the length of a normal pointer so it can be used -* efficiently with QList. -* -* @todo Maybe rename MessagePtr to MessageRef and this to MessagePtr? -*/ -class NullableMessageRef { -public: - NullableMessageRef() : d(nullptr) { } - NullableMessageRef(std::nullptr_t np) : d(np) { } - - /** - * @brief Take ownership of the given raw Message pointer. - * - * The message will be deleted when reference count falls to zero. - * @param msg - */ - explicit NullableMessageRef(Message *msg) - : d(msg) - { - if(d) { - Q_ASSERT(d->m_refcount==0); - ++d->m_refcount; - } - } - - NullableMessageRef(const MessagePtr &ptr) : d(&(*ptr)) { ++d->m_refcount; } - NullableMessageRef(const NullableMessageRef &ptr) : d(ptr.d) { if(d) ++d->m_refcount; } - - ~NullableMessageRef() - { - if(d) { - Q_ASSERT(d->m_refcount>0); - if(--d->m_refcount == 0) - delete d; - } - } - - NullableMessageRef &operator=(const NullableMessageRef &msg) - { - if(msg.d != d) { - if(d) { - Q_ASSERT(d->m_refcount>0); - if(--d->m_refcount == 0) - delete d; - } - d = msg.d; - if(d) - ++d->m_refcount; - } - return *this; - } - - NullableMessageRef &operator=(const MessagePtr &msg) - { - if(&(*msg) != d) { - if(d) { - Q_ASSERT(d->m_refcount>0); - if(--d->m_refcount == 0) - delete d; - } - d = &(*msg); - ++d->m_refcount; - } - return *this; - } - - inline bool isNull() const { return !d; } - - Message &operator*() const { Q_ASSERT(d); return *d; } - Message *operator->() const { Q_ASSERT(d); return d; } - - template msgtype &cast() const { Q_ASSERT(d); return static_cast(*d); } - - inline bool equals(const MessagePtr &m) const { return d && d->equals(*m); } - inline bool equals(const NullableMessageRef &m) const { return d && m.d && d->equals(*m); } + bool isControl() const; + bool isInCommandRange() const; + + unsigned int contextId() const; + void setContextId(unsigned int contextId); + + void setIndirectCompatFlag(); + + size_t length() const; + + bool equals(const Message &other) const; + + DP_MsgChat *toChat() const; + DP_MsgData *toData() const; + DP_MsgDrawDabsClassic *toDrawDabsClassic() const; + DP_MsgDrawDabsPixel *toDrawDabsPixel() const; + DP_MsgFeatureAccessLevels *toFeatureAccessLevels() const; + DP_MsgFillRect *toFillRect() const; + DP_MsgLayerAttributes *toLayerAttributes() const; + DP_MsgLayerTreeCreate *toLayerTreeCreate() const; + DP_MsgPrivateChat *toPrivateChat() const; + DP_MsgPutImage *toPutImage() const; + DP_MsgServerCommand *toServerCommand() const; + DP_MsgSessionOwner *toSessionOwner() const; + DP_MsgTrustedUsers *toTrustedUsers() const; + DP_MsgUserAcl *toUserAcl() const; + + bool serialize(QByteArray &buffer) const; + + bool shouldSmoothe() const; + + static void setUchars(size_t size, unsigned char *out, void *user); + static void setUint8s(int count, uint8_t *out, void *user); + static void setUint16s(int count, uint16_t *out, void *user); private: - Message *d; + explicit Message(DP_Message *cs); + + static unsigned char *getDeserializeBuffer(void *user, size_t size); + + DP_Message *m_data; }; -MessagePtr::MessagePtr(const NullableMessageRef &ref) - : d(&(*ref)) -{ - if(!d) - qFatal("MessagePtr::fromNullable(nullptr) called!"); - ++d->m_refcount; -} +Message makeChatMessage( + uint8_t contextId, uint8_t tflags, uint8_t oflags, const QString &message); -bool MessagePtr::equals(const NullableMessageRef &m) const { return !m.isNull() && d->equals(*m); } +Message makeDisconnectMessage( + uint8_t contextId, uint8_t reason, const QString &message); -} +Message makeJoinMessage( + uint8_t contextId, uint8_t flags, const QString &name, + const QByteArray &avatar); + +Message makeLeaveMessage(uint8_t contextId); + +Message makePingMessage(uint8_t contextId, bool isPong); + +Message makePrivateChatMessage( + uint8_t contextId, uint8_t target, uint8_t oflags, const QString &message); -Q_DECLARE_TYPEINFO(protocol::MessagePtr, Q_MOVABLE_TYPE); -Q_DECLARE_TYPEINFO(protocol::NullableMessageRef, Q_MOVABLE_TYPE); +Message makeServerCommandMessage(uint8_t contextId, const QJsonDocument &msg); + +Message +makeSessionOwnerMessage(uint8_t contextId, const QVector &users); + +Message +makeTrustedUsersMessage(uint8_t contextId, const QVector &users); + +} #endif diff --git a/src/libshared/net/messagequeue.cpp b/src/libshared/net/messagequeue.cpp index 72d8741c83..6799c1c9e9 100644 --- a/src/libshared/net/messagequeue.cpp +++ b/src/libshared/net/messagequeue.cpp @@ -1,63 +1,67 @@ // SPDX-License-Identifier: GPL-3.0-or-later #include "libshared/net/messagequeue.h" -#include "libshared/net/control.h" +#include "libshared/util/qtcompat.h" -#include #include +#include #include +#include #include -#ifndef NDEBUG -#include -#include -#endif - -namespace protocol { - -// Reserve enough buffer space for one complete message -static const int MAX_BUF_LEN = 1024*64 + protocol::Message::HEADER_LEN; - -MessageQueue::MessageQueue(QTcpSocket *socket, QObject *parent) - : QObject(parent), m_socket(socket), - m_pingTimer(nullptr), - m_lastRecvTime(0), - m_idleTimeout(0), m_pingSent(0), m_closeWhenReady(false), - m_ignoreIncoming(false), - m_decodeOpaque(false) +namespace net { + +MessageQueue::MessageQueue( + QTcpSocket *socket, bool decodeOpaque, QObject *parent) + : QObject(parent) + , m_socket(socket) + , m_decodeOpaque(decodeOpaque) + , m_smoothEnabled(false) + , m_smoothDrainRate(DEFAULT_SMOOTH_DRAIN_RATE) + , m_smoothTimer(nullptr) + , m_smoothMessagesToDrain(INT_MAX) + , m_contextId(0) + , m_pingTimer(nullptr) + , m_lastRecvTime(0) + , m_idleTimeout(0) + , m_pingSent(0) + , m_gracefullyDisconnecting(false) + , m_artificialLagMs(0) + , m_artificialLagTimer(nullptr) { - connect(socket, SIGNAL(readyRead()), this, SLOT(readData())); - connect(socket, SIGNAL(bytesWritten(qint64)), this, SLOT(dataWritten(qint64))); + connect(socket, &QTcpSocket::readyRead, this, &MessageQueue::readData); + connect( + socket, &QTcpSocket::bytesWritten, this, &MessageQueue::dataWritten); if(socket->inherits("QSslSocket")) { connect(socket, SIGNAL(encrypted()), this, SLOT(sslEncrypted())); } m_recvbuffer = new char[MAX_BUF_LEN]; - m_sendbuffer = new char[MAX_BUF_LEN]; m_recvbytes = 0; m_sentbytes = 0; - m_sendbuflen = 0; m_idleTimer = new QTimer(this); - connect(m_idleTimer, &QTimer::timeout, this, &MessageQueue::checkIdleTimeout); + m_idleTimer->setTimerType(Qt::CoarseTimer); + connect( + m_idleTimer, &QTimer::timeout, this, &MessageQueue::checkIdleTimeout); m_idleTimer->setInterval(1000); m_idleTimer->setSingleShot(false); - -#ifndef NDEBUG - m_randomlag = 0; -#endif } void MessageQueue::sslEncrypted() { - disconnect(m_socket, SIGNAL(bytesWritten(qint64)), this, SLOT(dataWritten(qint64))); - connect(m_socket, SIGNAL(encryptedBytesWritten(qint64)), this, SLOT(dataWritten(qint64))); + disconnect( + m_socket, &QTcpSocket::bytesWritten, this, &MessageQueue::dataWritten); + connect( + m_socket, SIGNAL(encryptedBytesWritten(qint64)), this, + SLOT(dataWritten(qint64))); } void MessageQueue::checkIdleTimeout() { - if(m_idleTimeout>0 && m_socket->state() == QTcpSocket::ConnectedState && idleTime() > m_idleTimeout) { + if(m_idleTimeout > 0 && m_socket->state() == QTcpSocket::ConnectedState && + idleTime() > m_idleTimeout) { qWarning("MessageQueue timeout"); m_socket->abort(); } @@ -67,7 +71,7 @@ void MessageQueue::setIdleTimeout(qint64 timeout) { m_idleTimeout = timeout; m_lastRecvTime = QDateTime::currentMSecsSinceEpoch(); - if(timeout>0) + if(timeout > 0) m_idleTimer->start(1000); else m_idleTimer->stop(); @@ -77,17 +81,64 @@ void MessageQueue::setPingInterval(int msecs) { if(!m_pingTimer) { m_pingTimer = new QTimer(this); + m_pingTimer->setTimerType(Qt::CoarseTimer); m_pingTimer->setSingleShot(false); - connect(m_pingTimer, SIGNAL(timeout()), this, SLOT(sendPing())); + connect(m_pingTimer, &QTimer::timeout, this, &MessageQueue::sendPing); } m_pingTimer->setInterval(msecs); m_pingTimer->start(msecs); } +void MessageQueue::setSmoothEnabled(bool enabled) +{ + m_smoothEnabled = enabled; + updateSmoothing(); +} + +void MessageQueue::setSmoothDrainRate(int smoothDrainRate) +{ + m_smoothDrainRate = qBound(0, smoothDrainRate, MAX_SMOOTH_DRAIN_RATE); + updateSmoothing(); +} + +void MessageQueue::updateSmoothing() +{ + bool enabled = m_smoothEnabled && m_smoothDrainRate > 0; + if(enabled && !m_smoothTimer) { + m_smoothTimer = new QTimer{this}; + m_smoothTimer->setTimerType(Qt::PreciseTimer); + m_smoothTimer->setSingleShot(false); + m_smoothTimer->setInterval(SMOOTHING_INTERVAL_MSEC); + connect( + m_smoothTimer, &QTimer::timeout, this, + &MessageQueue::receiveSmoothedMessages); + } else if(!enabled && m_smoothTimer) { + delete m_smoothTimer; + m_smoothTimer = nullptr; + if(!m_smoothBuffer.isEmpty()) { + m_inbox.append(m_smoothBuffer); + m_smoothBuffer.clear(); + emit messageAvailable(); + } + } +} + +void MessageQueue::setArtificialLagMs(int msecs) +{ + m_artificialLagMs = qMax(0, msecs); + if(m_artificialLagMs != 0 && !m_artificialLagTimer) { + m_artificialLagTimer = new QTimer(this); + m_artificialLagTimer->setTimerType(Qt::PreciseTimer); + m_artificialLagTimer->setSingleShot(true); + connect( + m_artificialLagTimer, &QTimer::timeout, this, + &MessageQueue::sendArtificallyLaggedMessages); + } +} + MessageQueue::~MessageQueue() { - delete [] m_recvbuffer; - delete [] m_sendbuffer; + delete[] m_recvbuffer; } bool MessageQueue::isPending() const @@ -95,68 +146,167 @@ bool MessageQueue::isPending() const return !m_inbox.isEmpty(); } -MessagePtr MessageQueue::getPending() +net::Message MessageQueue::shiftPending() { - return m_inbox.dequeue(); + return m_inbox.takeFirst(); } -void MessageQueue::send(const MessagePtr &message) +void MessageQueue::receive(net::MessageList &buffer) { - if(!m_closeWhenReady) { - m_outbox.enqueue(message); - if(m_sendbuflen==0) - writeData(); + buffer.swap(m_inbox); +} + +void MessageQueue::send(const net::Message &msg) +{ + sendMultiple(1, &msg); +} + +void MessageQueue::sendMultiple(int count, const net::Message *msgs) +{ + if(m_artificialLagMs == 0) { + enqueueMessages(count, msgs); + } else { + long long time = + QDateTime::currentMSecsSinceEpoch() + m_artificialLagMs; + for(int i = 0; i < count; ++i) { + m_artificialLagTimes.append(time); + m_artificialLagMessages.append(msgs[i]); + } + if(!m_artificialLagTimer->isActive()) { + m_artificialLagTimer->start(m_artificialLagMs); + } + } +} + +void MessageQueue::receiveSmoothedMessages() +{ + int count = m_smoothBuffer.size(); + bool smoothTimerActive = m_smoothTimer->isActive(); + if(count == 0) { + if(smoothTimerActive) { + m_smoothTimer->stop(); + } + } else { + if(m_smoothMessagesToDrain > count) { + m_inbox.append(m_smoothBuffer); + m_smoothBuffer.clear(); + } else { + // We only smoothe strokes, all other messages get flushed along. + int drainCount = 0; + int drainBuffer = 0; + do { + net::Message msg = m_smoothBuffer.takeFirst(); + if(msg.shouldSmoothe()) { + drainCount += drainBuffer + 1; + drainBuffer = 0; + } else { + ++drainBuffer; + } + m_inbox.append(msg); + } while(drainCount < m_smoothMessagesToDrain && + !m_smoothBuffer.isEmpty()); + } + + if(!m_smoothBuffer.isEmpty() && !smoothTimerActive) { + m_smoothTimer->start(); + } + emit messageAvailable(); } } -void MessageQueue::send(const MessageList &messages) +void MessageQueue::sendArtificallyLaggedMessages() { - if(!m_closeWhenReady) { - m_outbox << messages; - if(m_sendbuflen==0) + long long now = QDateTime::currentMSecsSinceEpoch(); + int count = m_artificialLagTimes.count(); + int i = 0; + while(i < count) { + long long time = m_artificialLagTimes[i]; + if(time <= now) { + ++i; + } else { + break; + } + } + + enqueueMessages(i, m_artificialLagMessages.constData()); + m_artificialLagTimes.remove(0, i); + m_artificialLagMessages.remove(0, i); + + if(i < count) { + m_artificialLagTimer->start(m_artificialLagTimes.first() - now); + } +} + +void MessageQueue::enqueueMessages(int count, const net::Message *msgs) +{ + if(!m_gracefullyDisconnecting) { + for(int i = 0; i < count; ++i) { + m_outbox.enqueue(msgs[i]); + } + if(m_sendbuffer.isEmpty()) { writeData(); + } } } -void MessageQueue::sendNow(MessagePtr msg) +void MessageQueue::sendPingMsg(bool pong) { - if(!m_closeWhenReady) { - m_outbox.prepend(msg); - if(m_sendbuflen==0) + if(m_artificialLagMs == 0) { + m_pings.enqueue(pong); + if(m_sendbuffer.isEmpty()) { writeData(); + } + } else { + // Not accurate, but probably good enough for development purposes. + send(net::makePingMessage(0, pong)); } } -void MessageQueue::sendDisconnect(int reason, const QString &message) +void MessageQueue::sendDisconnect( + GracefulDisconnect reason, const QString &message) { - send(MessagePtr(new protocol::Disconnect(0, protocol::Disconnect::Reason(reason), message))); - m_ignoreIncoming = true; + if(m_gracefullyDisconnecting) + qWarning("sendDisconnect: already disconnecting."); + + net::Message msg = net::makeDisconnectMessage(0, uint8_t(reason), message); + + qInfo( + "Sending disconnect message (reason=%d), will disconnect after queue " + "(%lld messages) is empty.", + int(reason), compat::cast(m_outbox.size() + m_pings.size())); + send(msg); + m_gracefullyDisconnecting = true; m_recvbytes = 0; + setSmoothEnabled(false); } void MessageQueue::sendPing() { - if(m_pingSent==0) { + if(m_pingSent == 0) { m_pingSent = QDateTime::currentMSecsSinceEpoch(); + } else { - // This shouldn't happen, but we'll resend a ping anyway just to be safe. + // This can happen if the other side's upload buffer is too full + // for the Pong to make it through in time. qWarning("sendPing(): reply to previous ping not yet received!"); } - sendNow(MessagePtr(new Ping(0, false))); + sendPingMsg(false); } int MessageQueue::uploadQueueBytes() const { - int total = m_socket->bytesToWrite() + m_sendbuflen - m_sentbytes; - for(const MessagePtr &msg : m_outbox) - total += msg->length(); + int total = m_socket->bytesToWrite() + m_sendbuffer.length() - m_sentbytes; + for(const net::Message &msg : m_outbox) + total += compat::castSize(msg.length()); + total += + m_pings.size() * (DP_MESSAGE_HEADER_LENGTH + DP_MSG_PING_STATIC_LENGTH); return total; } bool MessageQueue::isUploading() const { - return m_sendbuflen > 0 || m_socket->bytesToWrite() > 0; + return !m_sendbuffer.isEmpty() || m_socket->bytesToWrite() > 0; } qint64 MessageQueue::idleTime() const @@ -164,21 +314,34 @@ qint64 MessageQueue::idleTime() const return QDateTime::currentMSecsSinceEpoch() - m_lastRecvTime; } -void MessageQueue::readData() { - bool gotmessage = false; - int read, totalread=0; +int MessageQueue::haveWholeMessageToRead() +{ + if(m_recvbytes >= DP_MESSAGE_HEADER_LENGTH) { + int bodyLength = qFromBigEndian(m_recvbuffer); + int messageLength = bodyLength + DP_MESSAGE_HEADER_LENGTH; + if(m_recvbytes >= messageLength) { + return messageLength; + } + } + return 0; +} + +void MessageQueue::readData() +{ + int read, totalread = 0, gotmessages = 0; + bool smoothFlush = false; do { - // Read as much as fits in to the deserialization buffer - read = m_socket->read(m_recvbuffer+m_recvbytes, MAX_BUF_LEN-m_recvbytes); - if(read<0) { + // Read as much as fits in to the message buffer + read = m_socket->read( + m_recvbuffer + m_recvbytes, MAX_BUF_LEN - m_recvbytes); + if(read < 0) { emit socketError(m_socket->errorString()); return; } - if(m_ignoreIncoming) { - // Ignore incoming data mode is used when we're shutting down the connection - // but want to clear the upload queue - if(read>0) + if(m_gracefullyDisconnecting) { + // Ignore incoming data when we're in the process of disconnecting + if(read > 0) continue; else return; @@ -187,56 +350,118 @@ void MessageQueue::readData() { m_recvbytes += read; // Extract all complete messages - int len; - while(m_recvbytes >= Message::HEADER_LEN && m_recvbytes >= (len=Message::sniffLength(m_recvbuffer))) { + int messageLength; + while((messageLength = haveWholeMessageToRead()) != 0) { // Whole message received! - NullableMessageRef msg = Message::deserialize(reinterpret_cast(m_recvbuffer), m_recvbytes, m_decodeOpaque); - if(msg.isNull()) { - emit badData(len, uchar(m_recvbuffer[2]), uchar(m_recvbuffer[3])); + int type = static_cast(m_recvbuffer[2]); + if(type == MSG_TYPE_PING) { + // Pings are handled internally + if(messageLength != DP_MESSAGE_HEADER_LENGTH + 1) { + // Not a valid Ping message! + emit badData(messageLength, MSG_TYPE_PING, 0); + } else { + handlePing(m_recvbuffer[DP_MESSAGE_HEADER_LENGTH]); + } + + } else if(type == MSG_TYPE_DISCONNECT) { + // Graceful disconnects are also handled internally + if(messageLength < DP_MESSAGE_HEADER_LENGTH + 1) { + // We expected at least a reason! + emit badData(messageLength, MSG_TYPE_DISCONNECT, 0); + } else { + emit gracefulDisconnect( + GracefulDisconnect( + m_recvbuffer[DP_MESSAGE_HEADER_LENGTH]), + QString::fromUtf8( + m_recvbuffer + DP_MESSAGE_HEADER_LENGTH + 1, + messageLength - DP_MESSAGE_HEADER_LENGTH - 1)); + } } else { - if(msg->type() == MSG_PING) { - // Special handling for Ping messages - bool isPong = msg.cast().isPong(); - - if(isPong) { - if(m_pingSent==0) { - qWarning("Received Pong, but no Ping was sent!"); - - } else { - qint64 roundtrip = QDateTime::currentMSecsSinceEpoch() - m_pingSent; - m_pingSent = 0; - emit pingPong(roundtrip); + // The rest are normal messages + net::Message msg = net::Message::deserialize( + reinterpret_cast(m_recvbuffer), + m_recvbytes, m_decodeOpaque); + if(msg.isNull()) { + qWarning("Error deserializing message: %s", DP_error()); + emit badData( + messageLength, type, + static_cast(m_recvbuffer[3])); + } else { + if(m_smoothTimer) { + // Undos already have a delay because they require a + // round trip, we don't want to make them even slower. + bool ownUndoReceived = m_contextId != 0 && + msg.type() == DP_MSG_UNDO && + msg.contextId() == m_contextId; + if(ownUndoReceived) { + smoothFlush = true; } + m_smoothBuffer.append(msg); } else { - sendNow(MessagePtr(new Ping(0, true))); + m_inbox.append(msg); } - - } else { - m_inbox.enqueue(MessagePtr::fromNullable(msg)); - gotmessage = true; + ++gotmessages; } } - if(len < m_recvbytes) { + if(messageLength < m_recvbytes) { // Buffer contains more than one message - memmove(m_recvbuffer, m_recvbuffer+len, m_recvbytes-len); + memmove( + m_recvbuffer, m_recvbuffer + messageLength, + m_recvbytes - messageLength); } - m_recvbytes -= len; + + m_recvbytes -= messageLength; } - // All messages extracted from buffer (if there were any): - // see if there are more bytes in the socket buffer + // All whole messages extracted from the work buffer. + // There can still be more bytes in the socket buffer. totalread += read; - } while(read>0); + } while(read > 0); if(totalread) { m_lastRecvTime = QDateTime::currentMSecsSinceEpoch(); emit bytesReceived(totalread); } - if(gotmessage) - emit messageAvailable(); + if(gotmessages != 0) { + if(m_smoothTimer) { + if(smoothFlush) { + m_inbox.append(m_smoothBuffer); + m_smoothBuffer.clear(); + emit messageAvailable(); + m_smoothTimer->stop(); + } else { + m_smoothMessagesToDrain = + m_smoothBuffer.size() / m_smoothDrainRate; + if(!m_smoothTimer->isActive()) { + receiveSmoothedMessages(); + } + } + } else { + emit messageAvailable(); + } + } +} + +void MessageQueue::handlePing(bool isPong) +{ + if(isPong) { + // We got a Pong back: measure latency + if(m_pingSent == 0) { + // Lots of pings can have been queued up + qDebug("Received Pong, but no Ping was sent!"); + + } else { + qint64 roundtrip = QDateTime::currentMSecsSinceEpoch() - m_pingSent; + m_pingSent = 0; + emit pingPong(roundtrip); + } + } else { + // Reply to a Ping with a Pong + sendPingMsg(true); + } } void MessageQueue::dataWritten(qint64 bytes) @@ -244,46 +469,41 @@ void MessageQueue::dataWritten(qint64 bytes) emit bytesSent(bytes); // Write more once the buffer is empty - if(m_socket->bytesToWrite()==0) { - if(m_sendbuflen==0 && m_outbox.isEmpty()) + if(m_socket->bytesToWrite() == 0) { + if(m_sendbuffer.isEmpty() && !messagesInOutbox()) { emit allSent(); - else + if(m_gracefullyDisconnecting) { + qInfo("All sent, gracefully disconnecting."); + m_socket->disconnectFromHost(); + } + } else { writeData(); + } } } -void MessageQueue::writeData() { - int sentBatch = 0; +void MessageQueue::writeData() +{ bool sendMore = true; + int sentBatch = 0; - while(sendMore && sentBatch < 1024*64) { + while(sendMore && sentBatch < 1024 * 64) { sendMore = false; - if(m_sendbuflen==0 && !m_outbox.isEmpty()) { + if(m_sendbuffer.isEmpty() && messagesInOutbox()) { // Upload buffer is empty, but there are messages in the outbox Q_ASSERT(m_sentbytes == 0); - - MessagePtr msg = m_outbox.dequeue(); - m_sendbuflen = msg->serialize(m_sendbuffer); - Q_ASSERT(m_sendbuflen>0); - Q_ASSERT(m_sendbuflen <= MAX_BUF_LEN); - - if(msg->type() == protocol::MSG_DISCONNECT) { - // Automatically disconnect after Disconnect notification is sent - m_closeWhenReady = true; - m_outbox.clear(); + if(!dequeueFromOutbox().serialize(m_sendbuffer)) { + qWarning("Error serializing message: %s", DP_error()); + sendMore = messagesInOutbox(); + continue; } } - if(m_sentbytes < m_sendbuflen) { -#ifndef NDEBUG - // Debugging tool: simulate bad network connections by sleeping at odd times - if(m_randomlag>0) { - QThread::msleep(QRandomGenerator::global()->generate() % m_randomlag); - } -#endif - - const int sent = m_socket->write(m_sendbuffer+m_sentbytes, m_sendbuflen-m_sentbytes); - if(sent<0) { + if(m_sentbytes < m_sendbuffer.length()) { + const int sent = m_socket->write( + m_sendbuffer.constData() + m_sentbytes, + m_sendbuffer.length() - m_sentbytes); + if(sent < 0) { // Error emit socketError(m_socket->errorString()); return; @@ -291,21 +511,30 @@ void MessageQueue::writeData() { m_sentbytes += sent; sentBatch += sent; - Q_ASSERT(m_sentbytes <= m_sendbuflen); - if(m_sentbytes >= m_sendbuflen) { - // Complete message sent - m_sendbuflen=0; - m_sentbytes=0; - if(m_closeWhenReady) { - m_socket->disconnectFromHost(); + Q_ASSERT(m_sentbytes <= m_sendbuffer.length()); - } else { - sendMore = true; - } + if(m_sentbytes >= m_sendbuffer.length()) { + // Complete envelope sent + m_sendbuffer.clear(); + m_sentbytes = 0; + sendMore = messagesInOutbox(); } } } } +bool MessageQueue::messagesInOutbox() const +{ + return !m_outbox.isEmpty() || !m_pings.isEmpty(); } +net::Message MessageQueue::dequeueFromOutbox() +{ + if(m_pings.isEmpty()) { + return m_outbox.dequeue(); + } else { + return net::makePingMessage(0, m_pings.dequeue()); + } +} + +} diff --git a/src/libshared/net/messagequeue.h b/src/libshared/net/messagequeue.h index fe58cd16dc..d1e5aff66a 100644 --- a/src/libshared/net/messagequeue.h +++ b/src/libshared/net/messagequeue.h @@ -1,70 +1,76 @@ // SPDX-License-Identifier: GPL-3.0-or-later - -#ifndef DP_NET_MSGQUEUE_H -#define DP_NET_MSGQUEUE_H - +#ifndef LIBSHARED_MSGQUEUE_H +#define LIBSHARED_MSGQUEUE_H #include "libshared/net/message.h" - -#include #include +#include +#include class QTcpSocket; class QTimer; -namespace protocol { +namespace net { /** * A wrapper for an IO device for sending and receiving messages. */ class MessageQueue final : public QObject { -Q_OBJECT + Q_OBJECT public: + static constexpr int DEFAULT_SMOOTH_DRAIN_RATE = 20; + static constexpr int MAX_SMOOTH_DRAIN_RATE = 60; + + enum class GracefulDisconnect { + Error, // An error occurred + Kick, // client was kicked by the session operator + Shutdown, // server is shutting down + Other, // other unspecified error + }; + /** * @brief Create a message queue that wraps a TCP socket. * * The MessageQueue does not take ownership of the device. */ - explicit MessageQueue(QTcpSocket *socket, QObject *parent = nullptr); - MessageQueue(const MessageQueue&) = delete; + explicit MessageQueue( + QTcpSocket *socket, bool decodeOpaque, QObject *parent); ~MessageQueue() override; - /** - * @brief Automatically decode opaque messages? - * - * This should be used on the client side only. - * @param d - */ - void setDecodeOpaque(bool d) { m_decodeOpaque = d; } - /** * @brief Check if there are new messages available * @return true if getPending will return a message */ bool isPending() const; + net::Message shiftPending(); + + /** + * Get received messages, swaps (!) the given buffer with the inbox. + */ + void receive(net::MessageList &buffer); + /** - * Get the next message in the queue. - * @return message + * Enqueue a single message for sending. */ - MessagePtr getPending(); + void send(const net::Message &msg); /** - * Enqueue a message for sending. + * Enqueue multiple messages for sending. */ - void send(const MessagePtr &message); - void send(const MessageList &messages); + void sendMultiple(int count, const net::Message *msgs); /** * @brief Gracefully disconnect * - * This function enqueues the disconnect notification message. The connection will - * be automatically closed after the message has been sent. Additionally, it - * causes all incoming messages to be ignored. + * This function enqueues the disconnect notification message. The + * connection will be automatically closed after the message has been sent. + * Additionally, it causes all incoming messages to be ignored and no more + * data to be accepted for sending. * * @param reason * @param message */ - void sendDisconnect(int reason, const QString &message); + void sendDisconnect(GracefulDisconnect reason, const QString &message); /** * @brief Get the number of bytes in the upload queue @@ -78,15 +84,16 @@ Q_OBJECT bool isUploading() const; /** - * @brief Get the number of milliseconds since the last message sent by the remote end + * @brief Get the number of milliseconds since the last message sent by the + * remote end */ qint64 idleTime() const; /** * @brief Set the maximum time the remote end can be quiet before timing out * - * This can be used together with a keepalive message to detect disconnects more - * reliably than relying on TCP, which may have a very long timeout. + * This can be used together with a keepalive message to detect disconnects + * more reliably than relying on TCP, which may have a very long timeout. * * @param timeout timeout in milliseconds */ @@ -95,8 +102,8 @@ Q_OBJECT /** * @brief Set Ping interval in milliseconds * - * When ping interval is greater than zero, a Ping messages will automatically - * be sent. + * When ping interval is greater than zero, a Ping messages will + * automatically be sent. * * Note. This should be used by the client only. * @@ -104,9 +111,14 @@ Q_OBJECT */ void setPingInterval(int msecs); -#ifndef NDEBUG - void setRandomLag(uint lag) { m_randomlag = lag; } -#endif + void setSmoothEnabled(bool smoothingEnabled); + void setSmoothDrainRate(int smoothDrainRate); + + int artificalLagMs() { return m_artificialLagMs; } + + void setArtificialLagMs(int msecs); + + void setContextId(unsigned int contextId) { m_contextId = contextId; } public slots: /** @@ -114,7 +126,8 @@ public slots: * * Note. Use this function instead of creating the Ping message yourself * to make sure the roundtrip timer is set correctly! - * This function will not send another ping message until a reply has been received. + * This function will not send another ping message until a reply has been + * received. */ void sendPing(); @@ -153,31 +166,64 @@ public slots: /** * @brief A reply to our Ping was just received - * @param roundtripTime time now - ping sent time + * @param roundtripTime milliseconds since we sent our ping */ void pingPong(qint64 roundtripTime); + /** + * The server sent a graceful disconnect notification + */ + void gracefulDisconnect(GracefulDisconnect reason, const QString &message); + private slots: void readData(); void dataWritten(qint64); void sslEncrypted(); void checkIdleTimeout(); + void receiveSmoothedMessages(); + + void sendArtificallyLaggedMessages(); + private: - void sendNow(MessagePtr msg); + static constexpr int MAX_BUF_LEN = 0xffff + DP_MESSAGE_HEADER_LENGTH; + static constexpr int MSG_TYPE_DISCONNECT = 1; + static constexpr int MSG_TYPE_PING = 2; + static constexpr int SMOOTHING_INTERVAL_MSEC = 1000 / 60; + + void enqueueMessages(int count, const net::Message *msgs); + int haveWholeMessageToRead(); void writeData(); + bool messagesInOutbox() const; + net::Message dequeueFromOutbox(); - QTcpSocket *m_socket; + void handlePing(bool isPong); + void sendPingMsg(bool pong); + + void updateSmoothing(); - char *m_recvbuffer; // raw message reception buffer - char *m_sendbuffer; // raw message upload buffer - int m_recvbytes; // number of bytes in reception buffer - int m_sentbytes; // number of bytes in upload buffer already sent - int m_sendbuflen; // length of the data in the upload buffer + QTcpSocket *m_socket; + bool m_decodeOpaque; - QQueue m_inbox; // pending messages - QQueue m_outbox; // messages to be sent + char *m_recvbuffer; // raw message reception buffer + QByteArray m_sendbuffer; // raw message upload buffer + int m_recvbytes; // number of bytes in reception buffer + int m_sentbytes; // number of bytes in upload buffer already sent + + net::MessageList m_inbox; // received (complete) messages + QQueue m_outbox; // messages to be sent + QQueue m_pings; // pings and pongs to be sent + + // Smoothing of received messages. Depending on the server and network + // conditions, messages will arrive in chunks, which causes other people's + // strokes to appear choppy. Smoothing compensates for it, if enabled. + bool m_smoothEnabled; + int m_smoothDrainRate; + QTimer *m_smoothTimer; + net::MessageList m_smoothBuffer; + int m_smoothMessagesToDrain; + unsigned int m_contextId; QTimer *m_idleTimer; QTimer *m_pingTimer; @@ -185,17 +231,14 @@ private slots: qint64 m_idleTimeout; qint64 m_pingSent; - bool m_closeWhenReady; - bool m_ignoreIncoming; + bool m_gracefullyDisconnecting; - bool m_decodeOpaque; - -#ifndef NDEBUG - uint m_randomlag; -#endif + int m_artificialLagMs; + QVector m_artificialLagTimes; + QVector m_artificialLagMessages; + QTimer *m_artificialLagTimer; }; } #endif - diff --git a/src/libshared/net/meta.cpp b/src/libshared/net/meta.cpp deleted file mode 100644 index f1d18633d4..0000000000 --- a/src/libshared/net/meta.cpp +++ /dev/null @@ -1,249 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -#include "libshared/net/meta.h" -#include "libshared/net/textmode.h" - -#include -#include - -namespace protocol { - -UserJoin *UserJoin::deserialize(uint8_t ctx, const uchar *data, uint len) -{ - if(len<2) - return nullptr; - - const uint8_t flags = data[0]; - const uint nameLen = data[1]; - - // Name must be at least one character long, but avatar is optional - if(nameLen==0 || nameLen+2 > len) - return nullptr; - - const QByteArray name = QByteArray(reinterpret_cast(data)+2, nameLen); - const QByteArray avatar = QByteArray(reinterpret_cast(data)+2+nameLen, len-2-nameLen); - return new UserJoin(ctx, flags, name, avatar); -} - -int UserJoin::serializePayload(uchar *data) const -{ - uchar *ptr = data; - *(ptr++) = m_flags; - *(ptr++) = m_name.length(); - memcpy(ptr, m_name.constData(), m_name.length()); - ptr += m_name.length(); - memcpy(ptr, m_avatar.constData(), m_avatar.length()); - ptr += m_avatar.length(); - - return ptr - data; -} - -int UserJoin::payloadLength() const -{ - return 1 + 1 + m_name.length() + m_avatar.length(); -} - -Kwargs UserJoin::kwargs() const -{ - Kwargs kw; - kw["name"] = name(); - if(!m_avatar.isEmpty()) - kw["avatar"] = m_avatar.toBase64(); - QStringList flags; - if(isModerator()) - flags << "mod"; - if(isAuthenticated()) - flags << "auth"; - if(!flags.isEmpty()) - kw["flags"] = flags.join(','); - return kw; -} - -UserJoin *UserJoin::fromText(uint8_t ctx, const Kwargs &kwargs) -{ - QStringList flags = kwargs["flags"].split(','); - - return new UserJoin( - ctx, - (flags.contains("mod") ? FLAG_MOD : 0) | - (flags.contains("auth") ? FLAG_AUTH : 0), - kwargs["name"], - QByteArray::fromBase64(kwargs["avatar"].toLatin1()) - ); -} - - -SessionOwner *SessionOwner::deserialize(uint8_t ctx, const uchar *data, int len) -{ - if(len>255) - return nullptr; - - QList ids; - ids.reserve(len); - for(int i=0;i255) - return nullptr; - - QList ids; - ids.reserve(len); - for(int i=0;i(data)+2, len-2)); -} - -int Chat::serializePayload(uchar *data) const -{ - uchar *ptr = data; - *(ptr++) = transparentFlags(); - *(ptr++) = opaqueFlags(); - memcpy(ptr, m_msg.constData(), m_msg.length()); - ptr += m_msg.length(); - return ptr - data; -} - -int Chat::payloadLength() const -{ - return 2 + m_msg.length(); -} - -Kwargs Chat::kwargs() const -{ - Kwargs kw; - kw["message"] = message(); - QStringList flags; - if(isBypass()) - flags << "bypass"; - if(isShout()) - flags << "shout"; - if(isAction()) - flags << "action"; - if(isPin()) - flags << "pin"; - if(!flags.isEmpty()) - kw["flags"] = flags.join(','); - return kw; -} - -Chat *Chat::fromText(uint8_t ctx, const Kwargs &kwargs) -{ - QStringList flags = kwargs["flags"].split(','); - return new Chat( - ctx, - (flags.contains("bypass") ? FLAG_BYPASS : 0), - (flags.contains("shout") ? FLAG_SHOUT : 0) | - (flags.contains("action") ? FLAG_ACTION : 0) | - (flags.contains("pin") ? FLAG_PIN : 0), - kwargs["message"] - ); -} - -PrivateChat *PrivateChat::deserialize(uint8_t ctx, const uchar *data, uint len) -{ - if(len<3) - return nullptr; - return new PrivateChat(ctx, *(data+0), *(data+1), QByteArray(reinterpret_cast(data)+2, len-2)); -} - -int PrivateChat::serializePayload(uchar *data) const -{ - uchar *ptr = data; - *(ptr++) = target(); - *(ptr++) = opaqueFlags(); - memcpy(ptr, m_msg.constData(), m_msg.length()); - ptr += m_msg.length(); - return ptr - data; -} - -int PrivateChat::payloadLength() const -{ - return 2 + m_msg.length(); -} - -Kwargs PrivateChat::kwargs() const -{ - Kwargs kw; - kw["target"] = QString::number(target()); - kw["message"] = message(); - if(isAction()) kw["flags"] = "action"; - return kw; -} - -PrivateChat *PrivateChat::fromText(uint8_t ctx, const Kwargs &kwargs) -{ - QStringList flags = kwargs["flags"].split(','); - return new PrivateChat( - ctx, - kwargs["target"].toInt(), - (flags.contains("action") ? FLAG_ACTION : 0), - kwargs["message"] - ); -} - -} - diff --git a/src/libshared/net/meta.h b/src/libshared/net/meta.h deleted file mode 100644 index 1e55fb99f8..0000000000 --- a/src/libshared/net/meta.h +++ /dev/null @@ -1,291 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -#ifndef DP_NET_META_TRANSPARENT_H -#define DP_NET_META_TRANSPARENT_H - -#include "libshared/net/message.h" - -#include -#include - -namespace protocol { - -/** - * @brief Inform the client of a new user - * - * This message is sent only be the server. It associates a username - * with a context ID. - * - */ -class UserJoin final : public Message { -public: - static const uint8_t FLAG_AUTH = 0x01; // authenticated user (not a guest) - static const uint8_t FLAG_MOD = 0x02; // user is a moderator - static const uint8_t FLAG_BOT = 0x04; // user is a bot - - UserJoin(uint8_t ctx, uint8_t flags, const QByteArray &name, const QByteArray &avatar) : Message(MSG_USER_JOIN, ctx), m_name(name), m_avatar(avatar), m_flags(flags) { Q_ASSERT(name.length()>0 && name.length()<256); } - UserJoin(uint8_t ctx, uint8_t flags, const QString &name, const QByteArray &avatar=QByteArray()) : UserJoin(ctx, flags, name.toUtf8(), avatar) {} - - static UserJoin *deserialize(uint8_t ctx, const uchar *data, uint len); - static UserJoin *fromText(uint8_t ctx, const Kwargs &kwargs); - - QString name() const { return QString::fromUtf8(m_name); } - - QByteArray avatar() const { return m_avatar; } - - uint8_t flags() const { return m_flags; } - - bool isModerator() const { return m_flags & FLAG_MOD; } - bool isAuthenticated() const { return m_flags & FLAG_AUTH; } - bool isBot() const { return m_flags & FLAG_BOT; } - - QString messageName() const override { return QStringLiteral("join"); } - -protected: - int payloadLength() const override; - int serializePayload(uchar *data) const override; - Kwargs kwargs() const override; - -private: - QByteArray m_name; - QByteArray m_avatar; - uint8_t m_flags; -}; - -/** - * @brief Inform the client of a user leaving - * - * This message is sent only by the server. Upon receiving this message, - * clients will typically remove the user from the user listing. The client - * is also allowed to release resources associated with this context ID. - */ -class UserLeave final : public ZeroLengthMessage { -public: - explicit UserLeave(uint8_t ctx) : ZeroLengthMessage(MSG_USER_LEAVE, ctx) {} - - QString messageName() const override { return "leave"; } -}; - -/** - * @brief Session ownership change - * - * This message sets the users who have operator status. It can be - * sent by users who are already operators or by the server (ctx=0). - * - * The list of operators implicitly contains the user who sends the - * message, thus users cannot deop themselves. - * - * The server sanitizes the ID list so, when distributed to other users, - * it does not contain any duplicates or non-existing users. - */ -class SessionOwner final : public Message { -public: - SessionOwner(uint8_t ctx, QList ids) : Message(MSG_SESSION_OWNER, ctx), m_ids(ids) { } - - static SessionOwner *deserialize(uint8_t ctx, const uchar *data, int buflen); - static SessionOwner *fromText(uint8_t ctx, const Kwargs &kwargs); - - QList ids() const { return m_ids; } - void setIds(const QList ids) { m_ids = ids; } - - QString messageName() const override { return "owner"; } - -protected: - int payloadLength() const override; - int serializePayload(uchar *data) const override; - Kwargs kwargs() const override; - -private: - QList m_ids; -}; - -/** - * @brief List of trusted users - * - * This message sets the list of user who have been tagged as trusted, - * but who are not operators. The meaning of "trusted" is a mostly - * clientside concept, but the session can be configured to allow trusted - * users access to some operator commands. (Deputies) - * - * This command can be sent by operators or by the server (ctx=0). - * - * The server sanitizes the ID list so, when distributed to other users, - * it does not contain any duplicates or non-existing users. - */ -class TrustedUsers final : public Message { -public: - TrustedUsers(uint8_t ctx, QList ids) : Message(MSG_TRUSTED_USERS, ctx), m_ids(ids) { } - - static TrustedUsers *deserialize(uint8_t ctx, const uchar *data, int buflen); - static TrustedUsers *fromText(uint8_t ctx, const Kwargs &kwargs); - - QList ids() const { return m_ids; } - void setIds(const QList ids) { m_ids = ids; } - - QString messageName() const override { return "trusted"; } - -protected: - int payloadLength() const override; - int serializePayload(uchar *data) const override; - Kwargs kwargs() const override; - -private: - QList m_ids; -}; - -/** - * @brief A chat message - * - * Chat message sent by the server with the context ID 0 are server messages. - * (Typically a Command message is used for server announcements, but the Chat message - * is used for those messages that must be stored in the session history.) - */ -class Chat final : public Message { -public: - // Transparent flags: these affect serverside behavior - static const uint8_t FLAG_BYPASS = 0x01; // bypass session history and send directly to logged in users - - // Opaque flags: the server doesn't know anything about these - static const uint8_t FLAG_SHOUT = 0x01; // public announcement - static const uint8_t FLAG_ACTION = 0x02; // this is an "action message" (like /me in IRC) - static const uint8_t FLAG_PIN = 0x04; // pin this message - static const uint8_t FLAG_ALERT = 0x08; // high priority alert (can be send by operators only) - - Chat(uint8_t ctx, uint8_t tflags, uint8_t oflags, const QByteArray &msg) : Message(MSG_CHAT, ctx), m_tflags(tflags), m_oflags(oflags), m_msg(msg) {} - Chat(uint8_t ctx, uint8_t tflags, uint8_t oflags, const QString &msg) : Chat(ctx, tflags, oflags, msg.toUtf8()) {} - - //! Construct a regular chat message - static MessagePtr regular(uint8_t ctx, const QString &message, bool bypass) { return MessagePtr(new Chat(ctx, bypass ? FLAG_BYPASS : 0, 0, message.toUtf8())); } - - //! Construct a public announcement message - static MessagePtr announce(uint8_t ctx, const QString &message) { return MessagePtr(new Chat(ctx, 0, FLAG_SHOUT, message.toUtf8())); } - - //! Construct an action type message - static MessagePtr action(uint8_t ctx, const QString &message, bool bypass) { return MessagePtr(new Chat(ctx, bypass ? FLAG_BYPASS : 0, FLAG_ACTION, message.toUtf8())); } - - //! Construct a pinned message - static MessagePtr pin(uint8_t ctx, const QString &message) { return MessagePtr(new Chat(ctx, 0, FLAG_SHOUT|FLAG_PIN, message.toUtf8())); } - - static Chat *deserialize(uint8_t ctx, const uchar *data, uint len); - static Chat *fromText(uint8_t ctx, const Kwargs &kwargs); - - uint8_t transparentFlags() const { return m_tflags; } - uint8_t opaqueFlags() const { return m_oflags; } - - QString message() const { return QString::fromUtf8(m_msg); } - - /** - * @brief Is this a history bypass message? - * - * If this flag is set, the message is sent directly to other users and not included - * in the session history. - */ - bool isBypass() const { return m_tflags & FLAG_BYPASS; } - - /** - * @brief Is this a shout? - * - * Shout messages are highlighted so they stand out. Typically used - * without the BYPASS flag. - * - * Clientside only. - */ - bool isShout() const { return m_oflags & FLAG_SHOUT; } - - /** - * @brief Is this an action message? - * - * Clientside only. - */ - bool isAction() const { return m_oflags & FLAG_ACTION; } - - /** - * @brief Is this a pinned chat message? - * - * Clientside only. Requires OP privileges. - */ - bool isPin() const { return m_oflags & FLAG_PIN; } - - QString messageName() const override { return QStringLiteral("chat"); } - -protected: - int payloadLength() const override; - int serializePayload(uchar *data) const override; - Kwargs kwargs() const override; - -private: - uint8_t m_tflags; - uint8_t m_oflags; - QByteArray m_msg; -}; - -/** - * @brief A private chat message - * - * Note. This message type was added in protocol 4.21.2 (v. 2.1.0). For backward compatiblity, - * the server will not send any private messages from itself; it will only relay them from - * other users. - * - * Private messages always bypass the session history. - */ -class PrivateChat final : public Message { -public: - // Opaque flags: the server doesn't know anything about these - static const uint8_t FLAG_ACTION = 0x02; // this is an "action message" (like /me in IRC) - - PrivateChat(uint8_t ctx, uint8_t target, uint8_t oflags, const QByteArray &msg) : Message(MSG_PRIVATE_CHAT, ctx), m_target(target), m_oflags(oflags), m_msg(msg) {} - PrivateChat(uint8_t ctx, uint8_t target, uint8_t oflags, const QString &msg) : PrivateChat(ctx, target, oflags, msg.toUtf8()) {} - - //! Construct a regular chat message - static MessagePtr regular(uint8_t ctx, uint8_t target, const QString &message) { return MessagePtr(new PrivateChat(ctx, target, 0, message.toUtf8())); } - - //! Construct an action type message - static MessagePtr action(uint8_t ctx, uint8_t target, const QString &message) { return MessagePtr(new PrivateChat(ctx, target, FLAG_ACTION, message.toUtf8())); } - - static PrivateChat *deserialize(uint8_t ctx, const uchar *data, uint len); - static PrivateChat *fromText(uint8_t ctx, const Kwargs &kwargs); - - //! Recipient ID - uint8_t target() const { return m_target; } - - uint8_t opaqueFlags() const { return m_oflags; } - - QString message() const { return QString::fromUtf8(m_msg); } - - //! Is this an action message? (client side only) - bool isAction() const { return m_oflags & FLAG_ACTION; } - - QString messageName() const override { return QStringLiteral("pm"); } - -protected: - int payloadLength() const override; - int serializePayload(uchar *data) const override; - Kwargs kwargs() const override; - -private: - uint8_t m_target; - uint8_t m_oflags; - QByteArray m_msg; -}; - -/** - * @brief Soft reset point marker - * - * This message marks the point in the session history where soft reset occurs. - * Soft resetting is not actually implemented yet; this is here for forward compatiblity. - * - * All users should truncate their own session history when receiving this message, - * since undos cannot cross the reset boundary. - * - * The current client implementation handles the history truncation part. This is - * enough to be compatible with future clients capable of initiating soft reset. - */ -class SoftResetPoint final : public ZeroLengthMessage { -public: - explicit SoftResetPoint(uint8_t ctx) : ZeroLengthMessage(MSG_SOFTRESET, ctx) { } - QString messageName() const override { return QStringLiteral("softreset"); } -}; - -} - -#endif diff --git a/src/libshared/net/meta2.cpp b/src/libshared/net/meta2.cpp deleted file mode 100644 index 1bf5a1b4a0..0000000000 --- a/src/libshared/net/meta2.cpp +++ /dev/null @@ -1,291 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -#include "libshared/net/meta2.h" -#include "libshared/net/textmode.h" - -#include -#include - -namespace protocol { - -LaserTrail *LaserTrail::deserialize(uint8_t ctx, const uchar *data, uint len) -{ - if(len!=5) - return nullptr; - return new LaserTrail( - ctx, - qFromBigEndian(data+0), - *(data+4) - ); -} - -int LaserTrail::payloadLength() const -{ - return 4+1; -} - -int LaserTrail::serializePayload(uchar *data) const -{ - uchar *ptr = data; - qToBigEndian(m_color, ptr); ptr += 4; - *(ptr++) = m_persistence; - - return ptr-data; -} - -Kwargs LaserTrail::kwargs() const -{ - Kwargs kw; - kw["color"] = text::rgbString(m_color); - kw["persistence"] = QString::number(m_persistence); - return kw; -} - -LaserTrail *LaserTrail::fromText(uint8_t ctx, const Kwargs &kwargs) -{ - return new LaserTrail( - ctx, - text::parseColor(kwargs["color"]), - kwargs["persistence"].toInt() - ); -} - -MovePointer *MovePointer::deserialize(uint8_t ctx, const uchar *data, uint len) -{ - if(len!=8) - return nullptr; - return new MovePointer( - ctx, - qFromBigEndian(data+0), - qFromBigEndian(data+4) - ); -} - -int MovePointer::payloadLength() const -{ - return 2*4; -} - -int MovePointer::serializePayload(uchar *data) const -{ - uchar *ptr = data; - qToBigEndian(m_x, ptr); ptr += 4; - qToBigEndian(m_y, ptr); ptr += 4; - - return ptr-data; -} - -Kwargs MovePointer::kwargs() const -{ - Kwargs kw; - kw["x"] = QString::number(m_x); - kw["y"] = QString::number(m_y); - return kw; -} - -MovePointer *MovePointer::fromText(uint8_t ctx, const Kwargs &kwargs) -{ - return new MovePointer( - ctx, - kwargs["x"].toInt(), - kwargs["y"].toInt() - ); -} - -UserACL *UserACL::deserialize(uint8_t ctx, const uchar *data, uint len) -{ - if(len>255) - return nullptr; - - QList ids; - ids.reserve(len); - for(uint i=0;i 3+255) - return nullptr; - uint16_t id = qFromBigEndian(data+0); - uint8_t lock = data[2]; - QList exclusive; - for(uint i=3;i 0) { - kw["tier"] = tierName(tier()); - if(!m_exclusive.isEmpty()) - kw["exclusive"] = text::idListString(m_exclusive); - } - - return kw; -} - -LayerACL *LayerACL::fromText(uint8_t ctx, const Kwargs &kwargs) -{ - return new LayerACL( - ctx, - text::parseIdString16(kwargs["id"]), - kwargs["locked"] == "true", - tierFromName(kwargs["tier"]), - text::parseIdListString8(kwargs["exclusive"]) - ); -} - -FeatureAccessLevels *FeatureAccessLevels::deserialize(uint8_t ctx, const uchar *data, uint len) -{ - if(len != FEATURES) - return nullptr; - - return new FeatureAccessLevels(ctx, data); -} - -int FeatureAccessLevels::serializePayload(uchar *data) const -{ - memcpy(data, m_featureTiers, FEATURES); - return FEATURES; -} - -static const char *FEATURE_NAMES[FeatureAccessLevels::FEATURES] = { - "putimage", - "regionmove", - "resize", - "background", - "editlayers", - "ownlayers", - "createannotation", - "laser", - "undo" -}; - -Kwargs FeatureAccessLevels::kwargs() const -{ - Kwargs kw; - for(int i=0;i 0) - kw[FEATURE_NAMES[i]] = tierName(m_featureTiers[i]); - } - return kw; -} - -FeatureAccessLevels *FeatureAccessLevels::fromText(uint8_t ctx, const Kwargs &kwargs) -{ - uint8_t features[FEATURES]; - for(int i=0;i(data) - ); -} - -int DefaultLayer::payloadLength() const -{ - return 2; -} - -int DefaultLayer::serializePayload(uchar *data) const -{ - uchar *ptr = data; - qToBigEndian(m_id, ptr); ptr += 2; - return ptr - data; -} - -Kwargs DefaultLayer::kwargs() const -{ - Kwargs kw; - kw["id"] = text::idString(m_id); - return kw; -} - -DefaultLayer *DefaultLayer::fromText(uint8_t ctx, const Kwargs &kwargs) -{ - return new DefaultLayer( - ctx, - text::parseIdString16(kwargs["id"]) - ); -} - -} diff --git a/src/libshared/net/meta2.h b/src/libshared/net/meta2.h deleted file mode 100644 index df6811719c..0000000000 --- a/src/libshared/net/meta2.h +++ /dev/null @@ -1,207 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -#ifndef DP_NET_META_OPAQUE_H -#define DP_NET_META_OPAQUE_H - -#include "libshared/net/message.h" - -#include -#include - -namespace protocol { - -/** - * @brief Start/end drawing pointer laser trail - * - * This signals the beginning or the end of a laser pointer trail. The trail coordinates - * are sent with MovePointer messages. - * - * A nonzero persistence indicates the start of the trail and zero the end. - */ -class LaserTrail final : public Message { -public: - LaserTrail(uint8_t ctx, quint32 color, uint8_t persistence) : Message(MSG_LASERTRAIL, ctx), m_color(color), m_persistence(persistence) { } - static LaserTrail *deserialize(uint8_t ctx, const uchar *data, uint len); - static LaserTrail *fromText(uint8_t ctx, const Kwargs &kwargs); - - quint32 color() const { return m_color; } - uint8_t persistence() const { return m_persistence; } - - QString messageName() const override { return QStringLiteral("laser"); } - -protected: - int payloadLength() const override; - int serializePayload(uchar *data) const override; - Kwargs kwargs() const override; - -private: - quint32 m_color; - uint8_t m_persistence; -}; - -/** - * @brief Move user pointer - * - * This is message is used to update the position of the user pointer when no - * actual drawing is taking place. It is also used to draw the "laser pointer" trail. - * Note. This is a META message, since this is used for a temporary visual effect only, - * and thus doesn't affect the actual canvas content. - */ -class MovePointer final : public Message { -public: - MovePointer(uint8_t ctx, int32_t x, int32_t y) - : Message(MSG_MOVEPOINTER, ctx), m_x(x), m_y(y) - {} - - static MovePointer *deserialize(uint8_t ctx, const uchar *data, uint len); - static MovePointer *fromText(uint8_t ctx, const Kwargs &kwargs); - - int32_t x() const { return m_x; } - int32_t y() const { return m_y; } - - QString messageName() const override { return QStringLiteral("movepointer"); } - -protected: - int payloadLength() const override; - int serializePayload(uchar *data) const override; - Kwargs kwargs() const override; - -private: - int32_t m_x; - int32_t m_y; -}; - -/** - * @brief Set user specific locks - * - * This is an opaque meta command that contains a list of users to be locked. - * It can only be sent by session operators. - */ -class UserACL final : public Message { -public: - UserACL(uint8_t ctx, QList ids) : Message(MSG_USER_ACL, ctx), m_ids(ids) { } - - static UserACL *deserialize(uint8_t ctx, const uchar *data, uint len); - static UserACL *fromText(uint8_t ctx, const Kwargs &kwargs); - - QList ids() const { return m_ids; } - - QString messageName() const override { return QStringLiteral("useracl"); } - -protected: - int payloadLength() const override; - int serializePayload(uchar *data) const override; - Kwargs kwargs() const override; - -private: - QList m_ids; -}; - -/** - * @brief Change layer access control list - * - * This is an opaque meta command. It is used to set the general layer lock - * as well as give exclusive access to selected users. - * - * When the OWNLAYERS mode is set, any user can use this to change the ACLs on layers they themselves - * have created (identified by the ID prefix.) - * - * Using layer ID 0 sets or clears a general canvaswide lock. The tier and exclusive user list is not - * used in this case. - */ -class LayerACL final : public Message { -public: - LayerACL(uint8_t ctx, uint16_t id, bool locked, uint8_t tier, const QList &exclusive) - : LayerACL(ctx, id, (locked?0x80:0) | (tier&0x07), exclusive) - {} - - static LayerACL *deserialize(uint8_t ctx, const uchar *data, uint len); - static LayerACL *fromText(uint8_t ctx, const Kwargs &kwargs); - - uint16_t layer() const override { return m_id; } - bool locked() const { return m_flags & 0x80; } - uint8_t tier() const { return m_flags & 0x07;} - const QList exclusive() const { return m_exclusive; } - - QString messageName() const override { return QStringLiteral("layeracl"); } - -protected: - int payloadLength() const override; - int serializePayload(uchar *data) const override; - Kwargs kwargs() const override; - -private: - LayerACL(uint8_t ctx, uint16_t id, uint8_t flags, const QList &exclusive) - : Message(MSG_LAYER_ACL, ctx), m_id(id), m_flags(flags), m_exclusive(exclusive) - {} - - uint16_t m_id; - uint8_t m_flags; - QList m_exclusive; -}; - -/** - * @brief Change feature access levels - * - * This is an opaque meta command. - */ -class FeatureAccessLevels final : public Message { -public: - static const int FEATURES = 9; // Number of configurable features - - FeatureAccessLevels(uint8_t ctx, const uint8_t *featureTiers) - : Message(MSG_FEATURE_LEVELS, ctx) - { - for(int i=0;i=0 && featureIdx - -namespace protocol { - -OpaqueMessage::OpaqueMessage(MessageType type, uint8_t ctx, const uchar *payload, int payloadLen) - : Message(type, ctx), m_length(payloadLen) -{ - Q_ASSERT(type >= 64); - if(payloadLen>0) { - m_payload = new uchar[payloadLen]; - memcpy(m_payload, payload, payloadLen); - } else { - m_payload = nullptr; - } -} - -OpaqueMessage::~OpaqueMessage() -{ - delete []m_payload; -} - -NullableMessageRef OpaqueMessage::decode(MessageType type, uint8_t ctx, const uchar *data, uint len) -{ - Q_ASSERT(type>=64); - - Message *msg = nullptr; - - switch(type) { - case MSG_INTERVAL: msg = Interval::deserialize(ctx, data, len); break; - case MSG_LASERTRAIL: msg = LaserTrail::deserialize(ctx, data, len); break; - case MSG_MOVEPOINTER: msg = MovePointer::deserialize(ctx, data, len); break; - case MSG_MARKER: msg = Marker::deserialize(ctx, data, len); break; - case MSG_USER_ACL: msg = UserACL::deserialize(ctx, data, len); break; - case MSG_LAYER_ACL: msg = LayerACL::deserialize(ctx, data, len); break; - case MSG_FEATURE_LEVELS: msg = FeatureAccessLevels::deserialize(ctx, data, len); break; - case MSG_LAYER_DEFAULT: msg = DefaultLayer::deserialize(ctx, data, len); break; - case MSG_FILTERED: return Filtered::deserialize(ctx, data, len); - - case MSG_CANVAS_RESIZE: msg = CanvasResize::deserialize(ctx, data, len); break; - case MSG_LAYER_CREATE: msg = LayerCreate::deserialize(ctx, data, len); break; - case MSG_LAYER_ATTR: msg = LayerAttributes::deserialize(ctx, data, len); break; - case MSG_LAYER_RETITLE: msg = LayerRetitle::deserialize(ctx, data, len); break; - case MSG_LAYER_ORDER: msg = LayerOrder::deserialize(ctx, data, len); break; - case MSG_LAYER_DELETE: msg = LayerDelete::deserialize(ctx, data, len); break; - case MSG_LAYER_VISIBILITY: msg = LayerVisibility::deserialize(ctx, data, len); break; - case MSG_PUTIMAGE: msg = PutImage::deserialize(ctx, data, len); break; - case MSG_PEN_UP: msg = PenUp::deserialize(ctx, data, len); break; - case MSG_ANNOTATION_CREATE: msg = AnnotationCreate::deserialize(ctx, data, len); break; - case MSG_ANNOTATION_RESHAPE: msg = AnnotationReshape::deserialize(ctx, data, len); break; - case MSG_ANNOTATION_EDIT: msg = AnnotationEdit::deserialize(ctx, data, len); break; - case MSG_ANNOTATION_DELETE: msg = AnnotationDelete::deserialize(ctx, data, len); break; - case MSG_UNDOPOINT: msg = UndoPoint::deserialize(ctx, data, len); break; - case MSG_UNDO: msg = Undo::deserialize(ctx, data, len); break; - case MSG_FILLRECT: msg = FillRect::deserialize(ctx, data, len); break; - case MSG_REGION_MOVE: msg = MoveRegion::deserialize(ctx, data, len); break; - case MSG_PUTTILE: msg = PutTile::deserialize(ctx, data, len); break; - case MSG_CANVAS_BACKGROUND: msg = CanvasBackground::deserialize(ctx, data, len); break; - case MSG_DRAWDABS_CLASSIC: msg = DrawDabsClassic::deserialize(ctx, data, len); break; - case MSG_DRAWDABS_PIXEL: msg = DrawDabsPixel::deserialize(DabShape::Round, ctx, data, len); break; - case MSG_DRAWDABS_PIXEL_SQUARE: msg = DrawDabsPixel::deserialize(DabShape::Square, ctx, data, len); break; - default: qWarning("Unhandled opaque message type: %d", type); - } - - return NullableMessageRef(msg); -} - -NullableMessageRef OpaqueMessage::decode() const -{ - return decode(type(), contextId(), m_payload, m_length); -} - -int OpaqueMessage::payloadLength() const -{ - return m_length; -} - -int OpaqueMessage::serializePayload(uchar *data) const -{ - memcpy(data, m_payload, m_length); - return m_length; -} - -bool OpaqueMessage::payloadEquals(const Message &m) const -{ - const OpaqueMessage &om = static_cast(m); - if(m_length != om.m_length) - return false; - - return memcmp(m_payload, om.m_payload, m_length) == 0; -} - -} - diff --git a/src/libshared/net/opaque.h b/src/libshared/net/opaque.h deleted file mode 100644 index 0ea8ab3243..0000000000 --- a/src/libshared/net/opaque.h +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -#ifndef DP_NET_OPAQUE_H -#define DP_NET_OPAQUE_H - -#include "libshared/net/message.h" - -#include - -namespace protocol { - -/** - * @brief An opaque message - * - * This is treated as opaque binary data by the server. The client needs to be able - * to decode these, though. - * - */ -class OpaqueMessage final : public Message -{ -public: - OpaqueMessage(MessageType type, uint8_t ctx, const uchar *payload, int payloadLen); - ~OpaqueMessage() override; - OpaqueMessage(const OpaqueMessage &m) = delete; - OpaqueMessage &operator=(const OpaqueMessage &m) = delete; - - static NullableMessageRef decode(MessageType type, uint8_t ctx, const uchar *data, uint len); - - /** - * @brief Decode this message - * @return Message or nullptr if data is invalid - */ - NullableMessageRef decode() const; - - QString messageName() const override { return QStringLiteral("_opaque"); } - -protected: - int payloadLength() const override; - int serializePayload(uchar *data) const override; - bool payloadEquals(const Message &m) const override; - Kwargs kwargs() const override { return Kwargs(); } - -private: - uchar *m_payload; - int m_length; -}; - -} - -#endif diff --git a/src/libshared/net/recording.cpp b/src/libshared/net/recording.cpp deleted file mode 100644 index e149f235e3..0000000000 --- a/src/libshared/net/recording.cpp +++ /dev/null @@ -1,123 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -#include "libshared/net/recording.h" -#include "libshared/net/opaque.h" - -#include -#include - -namespace protocol { - -Interval *Interval::deserialize(uint8_t ctx, const uchar *data, uint len) -{ - if(len!=2) - return nullptr; - return new Interval(ctx, qFromBigEndian(data)); -} - -int Interval::payloadLength() const -{ - return 2; -} - -int Interval::serializePayload(uchar *data) const -{ - qToBigEndian(m_msecs, data); - return 2; -} - -Kwargs Interval::kwargs() const -{ - Kwargs kw; - kw["msecs"] = QString::number(m_msecs); - return kw; -} - -Interval *Interval::fromText(uint8_t ctx, const Kwargs &kwargs) -{ - return new Interval( - ctx, - kwargs["msecs"].toInt() - ); -} - -Marker *Marker::deserialize(uint8_t ctx, const uchar *data, uint len) -{ - return new Marker( - ctx, - QByteArray(reinterpret_cast(data), len) - ); -} - -int Marker::payloadLength() const -{ - return m_text.length(); -} - -int Marker::serializePayload(uchar *data) const -{ - memcpy(data, m_text.constData(), m_text.length()); - return m_text.length(); -} - -Kwargs Marker::kwargs() const -{ - Kwargs kw; - kw["text"] = text(); - return kw; -} - -Marker *Marker::fromText(uint8_t ctx, const Kwargs &kwargs) -{ - return new Marker( - ctx, - kwargs["text"] - ); -} - - -Filtered::Filtered(uint8_t ctx, uchar *payload, int payloadLen) - : Message(MSG_FILTERED, ctx), m_payload(payload), m_length(payloadLen) -{ } - -Filtered::~Filtered() -{ - delete []m_payload; -} - -NullableMessageRef Filtered::deserialize(uint8_t ctx, const uchar *data, uint len) -{ - if(len<1 || len > 0xffff) - return nullptr; - - uchar *payload = new uchar[len]; - memcpy(payload, data, len); - - return NullableMessageRef(new Filtered(ctx, payload, len)); -} - -NullableMessageRef Filtered::decodeWrapped() const -{ - // Note: technically non-opaque messages could be wrapped as well, - // but in practice they never are. Non-opaque messages are filtered - // by the server, so there is never need to filter them on the client side. - return OpaqueMessage::decode(wrappedType(), contextId(), m_payload+1, wrappedPayloadLength()); -} - -int Filtered::serializePayload(uchar *data) const -{ - memcpy(data, m_payload, m_length); - return m_length; -} - -bool Filtered::payloadEquals(const Message &m) const -{ - const Filtered &fm = static_cast(m); - if(m_length != fm.m_length) - return false; - - return memcmp(m_payload, fm.m_payload, m_length) == 0; -} - -} - diff --git a/src/libshared/net/recording.h b/src/libshared/net/recording.h deleted file mode 100644 index 78ebb1d1fc..0000000000 --- a/src/libshared/net/recording.h +++ /dev/null @@ -1,127 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -#ifndef DP_NET_RECORDING_H -#define DP_NET_RECORDING_H - -#include "libshared/net/message.h" - -#include -#include - -namespace protocol { - -/** - * @brief Event interval record - * - * This is used to preserve timing information in session recordings. - * - * Note. The maximum interval (using a single message) is about 65 seconds. - * Typically the intervals we want to store are a few seconds at most, so this should be enough. - */ -class Interval final : public Message -{ -public: - Interval(uint8_t ctx, uint16_t milliseconds) : Message(MSG_INTERVAL, ctx), m_msecs(milliseconds) {} - - static Interval *deserialize(uint8_t ctx, const uchar *data, uint len); - static Interval *fromText(uint8_t ctx, const Kwargs &kwargs); - - uint16_t milliseconds() const { return m_msecs; } - - QString messageName() const override { return QStringLiteral("interval"); } - -protected: - int payloadLength() const override; - int serializePayload(uchar *data) const override; - Kwargs kwargs() const override; - -private: - uint16_t m_msecs; -}; - -/** - * @brief A bookmark marker - * - * This is used to bookmark points in the session for quick access when playing back a recording. - */ -class Marker final : public Message -{ -public: - Marker(uint8_t ctx, const QString &text) : Message(MSG_MARKER, ctx), m_text(text.toUtf8()) { } - - static Marker *deserialize(uint8_t ctx, const uchar *data, uint len); - static Marker *fromText(uint8_t ctx, const Kwargs &kwargs); - - QString text() const { return QString::fromUtf8(m_text); } - - QString messageName() const override { return QStringLiteral("marker"); } - -protected: - int payloadLength() const override; - int serializePayload(uchar *data) const override; - Kwargs kwargs() const override; - -private: - QByteArray m_text; -}; - -/** - * @brief A message that has been filtered away (by the ACL filter) - * - * Filtered messages are retained in recordings, since they contain - * valuable debugging information. They are ignored by the client. - * When written in Text mode, they are written as comments. - */ -class Filtered final : public Message -{ -public: - Filtered(uint8_t ctx, uchar *payload, int payloadLen); - ~Filtered() override; - - static NullableMessageRef deserialize(uint8_t ctx, const uchar *data, uint len); - // Note: this type has no fromText function since it is serialized as a comment - - /** - * @brief Decode the wrapped message. - * - * Note: it is possible that the wrapped message is invalid. One additional byte - * is required to store the type of the wrapped message. If the original payload - * length was 65535 bytes, the last byte will be truncated. - * - * @return Message or nullptr if data is invalid - */ - NullableMessageRef decodeWrapped() const; - - /** - * @brief Get the type of the wrapped message - * - * Note: if there is no wrapped message, this returns 0 (MSG_COMMAND). - * Command messages should never be filtered anyway, so this is not - * a problem. - */ - MessageType wrappedType() const { return m_length > 0 ? MessageType(m_payload[0]) : MSG_COMMAND; } - - /** - * @brief Get the length of the wrapped message payload - * - * This does not include the extra byte that indicates the type - * of the message. - */ - int wrappedPayloadLength() const { return m_length-1; } - - QString messageName() const override { return QStringLiteral("filtered"); } - -protected: - int payloadLength() const override { return m_length; } - int serializePayload(uchar *data) const override; - Kwargs kwargs() const override { return Kwargs(); } - bool payloadEquals(const Message &m) const override; - -private: - uchar *m_payload; - int m_length; -}; - -} - -#endif diff --git a/src/libshared/net/servercmd.cpp b/src/libshared/net/servercmd.cpp new file mode 100644 index 0000000000..d1515c178e --- /dev/null +++ b/src/libshared/net/servercmd.cpp @@ -0,0 +1,345 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "libshared/net/servercmd.h" +#include "libshared/util/qtcompat.h" + +namespace net { + +net::Message ServerCommand::make( + const QString &cmd, const QJsonArray &args, const QJsonObject &kwargs) +{ + return ServerCommand{cmd, args, kwargs}.toMessage(); +} + +net::Message ServerCommand::makeKick(int target, bool ban) +{ + Q_ASSERT(target > 0 && target < 256); + QJsonObject kwargs; + if(ban) + kwargs["ban"] = true; + + return make("kick-user", QJsonArray() << target, kwargs); +} + +net::Message ServerCommand::makeUnban(int entryId) +{ + return make("remove-ban", QJsonArray() << entryId); +} + +net::Message ServerCommand::makeMute(int target, bool mute) +{ + return make("mute", QJsonArray() << target << mute); +} + +net::Message ServerCommand::makeAnnounce(const QString &url, bool privateMode) +{ + QJsonObject kwargs; + if(privateMode) + kwargs["private"] = true; + + return make("announce-session", QJsonArray() << url, kwargs); +} + +net::Message ServerCommand::makeUnannounce(const QString &url) +{ + return make("unlist-session", QJsonArray() << url); +} + +net::Message ServerCommand::toMessage() const +{ + QJsonObject data{{QStringLiteral("cmd"), cmd}}; + if(!args.isEmpty()) { + data["args"] = args; + } + if(!kwargs.isEmpty()) { + data["kwargs"] = kwargs; + } + return net::makeServerCommandMessage(0, QJsonDocument{data}); +} + +ServerCommand ServerCommand::fromMessage(const net::Message &msg) +{ + if(msg.isNull() || msg.type() != DP_MSG_SERVER_COMMAND) { + qWarning("ServerCommand::fromMessage: bad message"); + return ServerCommand{QString(), QJsonArray(), QJsonObject()}; + } + + size_t len; + const char *data = DP_msg_server_command_msg(msg.toServerCommand(), &len); + QByteArray bytes = QByteArray::fromRawData(data, compat::castSize(len)); + + QJsonParseError err; + const QJsonDocument doc = QJsonDocument::fromJson(bytes, &err); + if(err.error != QJsonParseError::NoError) { + qWarning( + "ServerReply::fromMessage JSON parsing error: %s", + qPrintable(err.errorString())); + return ServerCommand{QString(), QJsonArray(), QJsonObject()}; + } + + return fromJson(doc); +} + +ServerCommand ServerCommand::fromJson(const QJsonDocument &doc) +{ + QJsonObject data = doc.object(); + return ServerCommand{ + data["cmd"].toString(), data["args"].toArray(), + data["kwargs"].toObject()}; +} + + +ServerReply ServerReply::fromMessage(const net::Message &msg) +{ + if(msg.isNull() || msg.type() != DP_MSG_SERVER_COMMAND) { + qWarning("ServerReply::fromMessage: bad message"); + return ServerReply{ + ServerReply::ReplyType::Unknown, QString(), QJsonObject()}; + } + + size_t len; + const char *data = DP_msg_server_command_msg(msg.toServerCommand(), &len); + QByteArray bytes = QByteArray::fromRawData(data, compat::castSize(len)); + + QJsonParseError err; + const QJsonDocument doc = QJsonDocument::fromJson(bytes, &err); + if(err.error != QJsonParseError::NoError) { + qWarning( + "ServerReply::fromMessage JSON parsing error: %s", + qPrintable(err.errorString())); + return ServerReply{ + ServerReply::ReplyType::Unknown, QString(), QJsonObject()}; + } + + return fromJson(doc); +} + +ServerReply ServerReply::fromJson(const QJsonDocument &doc) +{ + QJsonObject data = doc.object(); + QString typestr = data.value("type").toString(); + + ServerReply r; + if(typestr == "login") + r.type = ServerReply::ReplyType::Login; + else if(typestr == "msg") + r.type = ServerReply::ReplyType::Message; + else if(typestr == "alert") + r.type = ServerReply::ReplyType::Alert; + else if(typestr == "error") + r.type = ServerReply::ReplyType::Error; + else if(typestr == "result") + r.type = ServerReply::ReplyType::Result; + else if(typestr == "log") + r.type = ServerReply::ReplyType::Log; + else if(typestr == "sessionconf") + r.type = ServerReply::ReplyType::SessionConf; + else if(typestr == "sizelimit") + r.type = ServerReply::ReplyType::SizeLimitWarning; + else if(typestr == "status") + r.type = ServerReply::ReplyType::Status; + else if(typestr == "reset") + r.type = ServerReply::ReplyType::Reset; + else if(typestr == "autoreset") + r.type = ServerReply::ReplyType::ResetRequest; + else if(typestr == "catchup") + r.type = ServerReply::ReplyType::Catchup; + else + r.type = ServerReply::ReplyType::Unknown; + + r.message = data.value("message").toString(); + r.reply = data; + return r; +} + +net::Message ServerReply::make(const QJsonObject &data) +{ + return net::makeServerCommandMessage(0, QJsonDocument{data}); +} + +net::Message ServerReply::makeError(const QString &message, const QString &code) +{ + return make( + {{QStringLiteral("type"), QStringLiteral("error")}, + {QStringLiteral("message"), message}, + {QStringLiteral("code"), code}}); +} + +net::Message +ServerReply::makeCommandError(const QString &command, const QString &message) +{ + return make( + {{QStringLiteral("type"), QStringLiteral("error")}, + {QStringLiteral("message"), + QStringLiteral("%1: %2").arg(command, message)}}); +} + +net::Message ServerReply::makeMessage(const QString &message) +{ + return make( + {{QStringLiteral("type"), QStringLiteral("message")}, + {QStringLiteral("message"), message}}); +} + +net::Message ServerReply::makeAlert(const QString &message) +{ + return make( + {{QStringLiteral("type"), QStringLiteral("alert")}, + {QStringLiteral("message"), message}}); +} + +net::Message ServerReply::makeCatchup(int count) +{ + return make( + {{QStringLiteral("type"), QStringLiteral("catchup")}, + {QStringLiteral("count"), count}}); +} + +net::Message ServerReply::makeLog(const QString &message, QJsonObject data) +{ + data[QStringLiteral("type")] = QStringLiteral("log"); + data[QStringLiteral("message")] = message; + return make(data); +} + +net::Message ServerReply::makeLoginGreeting( + const QString &message, int version, const QJsonArray &flags) +{ + return make( + {{QStringLiteral("type"), QStringLiteral("login")}, + {QStringLiteral("message"), message}, + {QStringLiteral("version"), version}, + {QStringLiteral("flags"), flags}}); +} + +net::Message ServerReply::makeLoginWelcome( + const QString &message, const QString &title, const QJsonArray &sessions) +{ + return make( + {{QStringLiteral("type"), QStringLiteral("login")}, + {QStringLiteral("message"), message}, + {QStringLiteral("title"), title}, + {QStringLiteral("sessions"), sessions}}); +} + +net::Message +ServerReply::makeLoginTitle(const QString &message, const QString &title) +{ + return make( + {{QStringLiteral("type"), QStringLiteral("login")}, + {QStringLiteral("message"), message}, + {QStringLiteral("title"), title}}); +} + +net::Message ServerReply::makeLoginSessions( + const QString &message, const QJsonArray &sessions) +{ + return make( + {{QStringLiteral("type"), QStringLiteral("login")}, + {QStringLiteral("message"), message}, + {QStringLiteral("sessions"), sessions}}); +} + +net::Message ServerReply::makeLoginRemoveSessions( + const QString &message, const QJsonArray &remove) +{ + return make( + {{QStringLiteral("type"), QStringLiteral("login")}, + {QStringLiteral("message"), message}, + {QStringLiteral("remove"), remove}}); +} + +net::Message +ServerReply::makeReset(const QString &message, const QString &state) +{ + return make( + {{QStringLiteral("type"), QStringLiteral("reset")}, + {QStringLiteral("message"), message}, + {QStringLiteral("state"), state}}); +} + +net::Message ServerReply::makeResetRequest(int maxSize, bool query) +{ + return make( + {{QStringLiteral("type"), QStringLiteral("autoreset")}, + {QStringLiteral("maxSize"), maxSize}, + {QStringLiteral("query"), query}}); +} + +net::Message ServerReply::makeResultPasswordNeeded( + const QString &message, const QString &state) +{ + return make( + {{QStringLiteral("type"), QStringLiteral("result")}, + {QStringLiteral("message"), message}, + {QStringLiteral("state"), state}}); +} + +net::Message ServerReply::makeResultLoginOk( + const QString &message, const QString &state, const QJsonArray &flags, + const QString &ident, bool guest) +{ + return make( + {{QStringLiteral("type"), QStringLiteral("result")}, + {QStringLiteral("message"), message}, + {QStringLiteral("state"), state}, + {QStringLiteral("flags"), flags}, + {QStringLiteral("ident"), ident}, + {QStringLiteral("guest"), guest}}); +} + +net::Message ServerReply::makeResultExtAuthNeeded( + const QString &message, const QString &state, const QString &extauthurl, + const QString &nonce, const QString &group, bool avatar) +{ + return make( + {{QStringLiteral("type"), QStringLiteral("result")}, + {QStringLiteral("message"), message}, + {QStringLiteral("state"), state}, + {QStringLiteral("extauthurl"), extauthurl}, + {QStringLiteral("nonce"), nonce}, + {QStringLiteral("group"), group}, + {QStringLiteral("avatar"), avatar}}); +} + +net::Message ServerReply::makeResultJoinHost( + const QString &message, const QString &state, const QJsonObject &join) +{ + return make( + {{QStringLiteral("type"), QStringLiteral("result")}, + {QStringLiteral("message"), message}, + {QStringLiteral("state"), state}, + {QStringLiteral("join"), join}}); +} + +net::Message +ServerReply::makeResultStartTls(const QString &message, bool startTls) +{ + return make( + {{QStringLiteral("type"), QStringLiteral("result")}, + {QStringLiteral("message"), message}, + {QStringLiteral("startTls"), startTls}}); +} + +net::Message ServerReply::makeSessionConf(const QJsonObject &config) +{ + return make( + {{QStringLiteral("type"), QStringLiteral("sessionconf")}, + {QStringLiteral("config"), config}}); +} + +net::Message ServerReply::makeSizeLimitWarning(int size, int maxSize) +{ + return make( + {{QStringLiteral("type"), QStringLiteral("sizelimit")}, + {QStringLiteral("size"), size}, + {QStringLiteral("maxSize"), maxSize}}); +} + +net::Message ServerReply::makeStatusUpdate(int size) +{ + return make( + {{QStringLiteral("type"), QStringLiteral("status")}, + {QStringLiteral("size"), size}}); +} + +} diff --git a/src/libshared/net/servercmd.h b/src/libshared/net/servercmd.h new file mode 100644 index 0000000000..0e217648f2 --- /dev/null +++ b/src/libshared/net/servercmd.h @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#ifndef LIBSHARED_NET_SERVERCMD_H +#define LIBSHARED_NET_SERVERCMD_H +#include "libshared/net/message.h" +#include +#include +#include + +namespace net { + +/** + * @brief A command sent to the server using the (Control) Command message + */ +struct ServerCommand { + QString cmd; + QJsonArray args; + QJsonObject kwargs; + + static ServerCommand fromMessage(const net::Message &msg); + static ServerCommand fromJson(const QJsonDocument &doc); + net::Message toMessage() const; + + // Convenience functions_ + static net::Message make( + const QString &cmd, const QJsonArray &args = QJsonArray(), + const QJsonObject &kwargs = QJsonObject()); + + //! Kick (and optionally ban) a user from the session + static net::Message makeKick(int target, bool ban); + + //! Remove a ban entry + static net::Message makeUnban(int entryId); + + //! (Un)mute a user + static net::Message makeMute(int target, bool mute); + + //! Request the server to announce this session at a listing server + static net::Message makeAnnounce(const QString &url, bool privateMode); + + //! Request the server to remove an announcement at the listing server + static net::Message makeUnannounce(const QString &url); +}; + +/** + * @brief A reply or notification from the server received with the Command + * message + */ +struct ServerReply { + enum class ReplyType { + Unknown, + Login, // used during the login phase + Message, // general chat type notifcation message + Alert, // urgen notification message + Error, // error occurred + Result, // command result + Log, // server log message + SessionConf, // session configuration update + SizeLimitWarning, // session history size nearing limit (deprecated) + Status, // periodic status update + Reset, // session reset state + Catchup, // number of messages queued for upload (use for progress bars) + ResetRequest, // request client to perform a reset + } type; + QString message; + QJsonObject reply; + + static ServerReply fromMessage(const net::Message &msg); + static ServerReply fromJson(const QJsonDocument &doc); + + static net::Message make(const QJsonObject &data); + + static net::Message makeError(const QString &message, const QString &code); + + static net::Message + makeCommandError(const QString &command, const QString &message); + + static net::Message makeMessage(const QString &message); + + static net::Message makeAlert(const QString &message); + + static net::Message makeCatchup(int count); + + static net::Message makeLog(const QString &message, QJsonObject data); + + static net::Message makeLoginGreeting( + const QString &message, int version, const QJsonArray &flags); + + static net::Message makeLoginWelcome( + const QString &message, const QString &title, + const QJsonArray &sessions); + + static net::Message + makeLoginTitle(const QString &message, const QString &title); + + static net::Message + makeLoginSessions(const QString &message, const QJsonArray &sessions); + + static net::Message + makeLoginRemoveSessions(const QString &message, const QJsonArray &remove); + + static net::Message makeReset(const QString &message, const QString &state); + + static net::Message makeResetRequest(int maxSize, bool query); + + static net::Message + makeResultPasswordNeeded(const QString &message, const QString &state); + + static net::Message makeResultLoginOk( + const QString &message, const QString &state, const QJsonArray &flags, + const QString &ident, bool guest); + + static net::Message makeResultExtAuthNeeded( + const QString &message, const QString &state, const QString &extauthurl, + const QString &nonce, const QString &group, bool avatar); + + static net::Message makeResultJoinHost( + const QString &message, const QString &state, const QJsonObject &join); + + static net::Message + makeResultStartTls(const QString &message, bool startTls); + + static net::Message makeSessionConf(const QJsonObject &config); + + static net::Message makeSizeLimitWarning(int size, int maxSize); + + static net::Message makeStatusUpdate(int size); +}; + +} + +#endif diff --git a/src/libshared/net/textmode.cpp b/src/libshared/net/textmode.cpp deleted file mode 100644 index c0d3e9fccc..0000000000 --- a/src/libshared/net/textmode.cpp +++ /dev/null @@ -1,263 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -#include "libshared/net/textmode.h" - -#include "libshared/net/meta.h" -#include "libshared/net/meta2.h" -#include "libshared/net/brushes.h" -#include "libshared/net/annotation.h" -#include "libshared/net/layer.h" -#include "libshared/net/image.h" -#include "libshared/net/recording.h" -#include "libshared/net/undo.h" -#include "libshared/util/qtcompat.h" - -namespace protocol { -namespace text { - -Parser::Result Parser::parseLine(const QString &line) -{ - switch(m_state) { - case ExpectCommand: { - if(line.isEmpty() || line.at(0) == '#') - return Result { Result::Skip, nullptr }; - - if(line.at(0) == '!') { - // Metadata line - int i = line.indexOf('='); - if(i>1) { - QString key = line.mid(1, i-1); - QString value = line.mid(i+1); - m_metadata[key] = value; - } - return Result { Result::Skip, nullptr }; - } - - QStringList tokens = line.split(' ', compat::SkipEmptyParts); - if(tokens.length() < 2) { - m_error = "Expected at least two tokens"; - return Result { Result::Error, nullptr }; - } - - // Get context ID - { - bool ok; - int ctxId = tokens[0].toInt(&ok); - if(!ok || ctxId<0 || ctxId>255) { - m_error = "Invalid context id: " + tokens[0]; - return Result { Result::Error, nullptr }; - } - m_ctx = ctxId; - } - - // Get message name - m_cmd = tokens[1]; - m_kwargs = Kwargs(); - m_dabs = QStringList(); - - // Check if this is a multiline message - bool multiline = false; - if(tokens.size() > 2 && tokens.last() == "{") { - tokens.removeLast(); - multiline = true; - } - - // Extract named arguments - for(int i=2;i ids) -{ - QStringList sl; - sl.reserve(ids.length()); - for(const uint8_t i : ids) - sl << QString::number(i); - return sl.join(','); -} - -QString idListString(QList ids) -{ - QStringList sl; - sl.reserve(ids.length()); - for(const uint16_t i : ids) - sl << idString(i); - return sl.join(','); -} - -QString rgbString(quint32 color) -{ - return QStringLiteral("#%1").arg(color & 0x00ffffff, 6, 16, QLatin1Char('0')); -} - -QString argbString(quint32 color) -{ - if((color & 0xff000000) == 0xff000000) - return rgbString(color); - else - return QStringLiteral("#%1").arg(color, 8, 16, QLatin1Char('0')); -} - -quint32 parseColor(const QString &color) -{ - if((color.length() == 7 || color.length() == 9) && color.at(0) == '#') { - bool ok; - quint32 c = compat::stringSlice(color, 1).toUInt(&ok, 16); - if(ok) - return color.length() == 7 ? 0xff000000 | c : c; - } - return 0; -} - -QList parseIdListString8(const QString &ids) -{ - QList list; - for(const QString &idstr : ids.split(',')) { - bool ok; - int id = idstr.toInt(&ok); - if(ok && id>=0 && id<256) - list << id; - } - return list; -} - -uint16_t parseIdString16(const QString &idstr, bool *allOk) -{ - bool ok; - int id; - if(idstr.startsWith("0x")) { - id = compat::stringSlice(idstr, 2).toInt(&ok, 16); - } else - id = idstr.toInt(&ok); - - if(ok && id>=0 && id<=0xffff) { - if(allOk) - *allOk = true; - return id; - } else { - if(allOk) - *allOk = false; - return 0; - } -} - -QList parseIdListString16(const QString &ids) -{ - QList list; - for(const QString &idstr : ids.split(',')) { - bool ok; - uint16_t id = parseIdString16(idstr, &ok); - if(ok) - list << id; - } - return list; -} - -} -} - diff --git a/src/libshared/net/textmode.h b/src/libshared/net/textmode.h deleted file mode 100644 index fc17a4ae2f..0000000000 --- a/src/libshared/net/textmode.h +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -#ifndef DP_NET_TEXTMODE_H -#define DP_NET_TEXTMODE_H - -#include "libshared/net/message.h" - -namespace protocol { -namespace text { - -/** - * Text mode file parser - */ -class Parser { -public: - struct Result { - enum { - Ok, - Skip, - Error, - NeedMore - } status; - NullableMessageRef msg; - }; - - Result parseLine(const QString &line); - QString errorString() const { return m_error; } - - Kwargs metadata() const { return m_metadata; } - - Parser() : m_state(ExpectCommand), m_ctx(0) { } - -private: - enum { - ExpectCommand, - ExpectKwargLine, - ExpectDab, - } m_state; - - Kwargs m_metadata; - QString m_error; - QString m_cmd; - Kwargs m_kwargs; - QStringList m_dabs; - int m_ctx; -}; - -// Formatting helper functions -inline QString idString(uint16_t id) { return QStringLiteral("0x%1").arg(id, 4, 16, QLatin1Char('0')); } -QString idListString(const QList ids); -QString idListString(const QList ids); -QString rgbString(quint32 color); -QString argbString(quint32 color); -inline QString decimal(uint8_t value) { return QString::number(value/255.0*100.0, 'f', 2); } - -// Parsing helper functions -uint16_t parseIdString16(const QString &id, bool *ok=nullptr); -QList parseIdListString8(const QString &ids); -QList parseIdListString16(const QString &ids); -quint32 parseColor(const QString &color); -inline uint8_t parseDecimal8(const QString &str) { return qBound(0, qRound(str.toFloat() / 100.0 * 255), 255); } - -} -} - -#endif - diff --git a/src/libshared/net/undo.cpp b/src/libshared/net/undo.cpp deleted file mode 100644 index d95195f673..0000000000 --- a/src/libshared/net/undo.cpp +++ /dev/null @@ -1,56 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -#include "libshared/net/undo.h" - -namespace protocol { - -Undo *Undo::deserialize(uint8_t ctx, const uchar *data, uint len) -{ - if(len!=2) - return nullptr; - return new Undo( - ctx, - data[0], - data[1] - ); -} - -int Undo::payloadLength() const -{ - return 2; -} - -int Undo::serializePayload(uchar *data) const -{ - uchar *ptr = data; - *(ptr++) = m_override; - *(ptr++) = m_redo; - return ptr-data; -} - -bool Undo::payloadEquals(const Message &m) const -{ - const Undo &u = static_cast(m); - return - overrideId() == u.overrideId() && - isRedo() == u.isRedo(); -} - -Kwargs Undo::kwargs() const -{ - Kwargs kw; - if(overrideId()>0) - kw["override"] = QString::number(overrideId()); - return kw; -} - -Undo *Undo::fromText(uint8_t ctx, const Kwargs &kwargs, bool redo) -{ - return new Undo( - ctx, - kwargs["override"].toInt(), - redo - ); -} - -} diff --git a/src/libshared/net/undo.h b/src/libshared/net/undo.h deleted file mode 100644 index 59aab58993..0000000000 --- a/src/libshared/net/undo.h +++ /dev/null @@ -1,74 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -#ifndef DP_NET_UNDO_H -#define DP_NET_UNDO_H - -#include "libshared/net/message.h" - -namespace protocol { - -/** - * @brief Undo history depth - * - * To ensure undo works consistently, each client must store at least this many - * undo points in their session history, but also limit the functional undo depth - * to this many points. - */ -static const int UNDO_DEPTH_LIMIT = 30; - -/** - * @brief Undo demarcation point - * - * The client sends an UndoPoint message to signal the start of an undoable sequence. - */ -class UndoPoint final : public ZeroLengthMessage -{ -public: - UndoPoint(uint8_t ctx) : ZeroLengthMessage(MSG_UNDOPOINT, ctx) {} - - QString messageName() const override { return QStringLiteral("undopoint"); } -}; - -/** - * @brief Undo or redo actions - * - */ -class Undo final : public Message -{ -public: - Undo(uint8_t ctx, uint8_t override, bool redo) : Message(MSG_UNDO, ctx), m_override(override), m_redo(redo) { } - - static Undo *deserialize(uint8_t ctx, const uchar *data, uint len); - static Undo *fromText(uint8_t ctx, const Kwargs &kwargs, bool redo); - - /** - * @brief override user ID - * - * This is used by session operators to undo actions by other - * users. This should be zero when undoing one's own actions. - * - * @return context id - */ - uint8_t overrideId() const { return m_override; } - - /** - * @brief Is this a redo operation? - */ - bool isRedo() const { return m_redo; } - - QString messageName() const override { return m_redo ? QStringLiteral("redo") : QStringLiteral("undo"); } - -protected: - int payloadLength() const override; - int serializePayload(uchar *data) const override; - bool payloadEquals(const Message &m) const override; - Kwargs kwargs() const override; - -private: - uint8_t m_override; - uint8_t m_redo; -}; - -} - -#endif diff --git a/src/libshared/record/header.cpp b/src/libshared/record/header.cpp deleted file mode 100644 index 4e0143f602..0000000000 --- a/src/libshared/record/header.cpp +++ /dev/null @@ -1,162 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -#include "libshared/record/header.h" -#include "libshared/net/protover.h" -#include "libshared/net/message.h" -#include "libshared/util/qtcompat.h" -#include "cmake-config/config.h" - -#include -#include -#include -#include -#include - -#include - -namespace recording { - -QJsonObject readRecordingHeader(QIODevice *file) -{ - Q_ASSERT(file && file->isOpen()); - - // Read magic bytes "DPREC\0" - char buf[6]; - if(file->read(buf, 6) != 6) - return QJsonObject(); - - if(memcmp(buf, "DPREC", 6) != 0) - return QJsonObject(); - - // Read metadata block - if(file->read(buf, 2) != 2) - return QJsonObject(); - - const quint16 metadatalen = qFromBigEndian(reinterpret_cast(buf)); - - QByteArray metadatabuf = file->read(metadatalen); - if(metadatabuf.length() != metadatalen) - return QJsonObject(); - - QJsonParseError jsonError; - QJsonDocument doc = QJsonDocument::fromJson(metadatabuf, &jsonError); - - if(jsonError.error != QJsonParseError::NoError) { - qWarning() << jsonError.errorString(); - return QJsonObject(); - } - - if(!doc.isObject()) { - qWarning() << "Recording metadata didn't contain an object!"; - return QJsonObject(); - } - - return doc.object(); -} - -static QJsonObject withDefaults(const QJsonObject metadata) -{ - QJsonObject header = metadata; - if(!header.contains("version")) - header["version"] = protocol::ProtocolVersion::current().asString(); - - if(!header.contains("writerversion")) - header["writerversion"] = cmake_config::version(); - - return header; -} - -bool writeRecordingHeader(QIODevice *file, const QJsonObject &metadata) -{ - Q_ASSERT(file && file->isOpen()); - - // Format identification - const char *MAGIC = "DPREC"; - file->write(MAGIC, 6); - - // Metadata block - QJsonObject md = withDefaults(metadata); - - QByteArray metadatabuf = QJsonDocument(md).toJson(QJsonDocument::Compact); - - if(metadatabuf.length() > 0xffff) { - qWarning("Recording metadata block too long (%lld)", compat::cast(metadatabuf.length())); - return false; - } - - uchar lenbuf[2]; - qToBigEndian(quint16(metadatabuf.length()), lenbuf); - - if(file->write(reinterpret_cast(lenbuf), 2) != 2) - return false; - - if(file->write(metadatabuf) != metadatabuf.length()) - return false; - - return true; -} - -bool writeTextHeader(QIODevice *file, const QJsonObject &metadata) -{ - Q_ASSERT(file && file->isOpen()); - QJsonObject md = withDefaults(metadata); - QMapIterator i(md.toVariantMap()); - while(i.hasNext()) { - i.next(); - QByteArray line = QString("!%1=%2\n").arg(i.key(), i.value().toString()).toUtf8(); - if(file->write(line) != line.length()) - return false; - } - return file->write("\n", 1) == 1; -} - -bool readRecordingMessage(QIODevice *file, QByteArray &buffer) -{ - Q_ASSERT(file && file->isOpen()); - - // Read length and type header - if(buffer.length() < protocol::Message::HEADER_LEN) - buffer.resize(128); - - if(file->read(buffer.data(), protocol::Message::HEADER_LEN) != protocol::Message::HEADER_LEN) - return false; - - const int len = protocol::Message::sniffLength(buffer.constData()); - - if(buffer.length() < len) - buffer.resize(len); - - // Read message payload - const int payloadlen = len - protocol::Message::HEADER_LEN; - if(file->read(buffer.data()+protocol::Message::HEADER_LEN, payloadlen) != payloadlen) - return false; - - return true; -} - -int skipRecordingMessage(QIODevice *file, uint8_t *msgType, uint8_t *ctxId) -{ - Q_ASSERT(file && file->isOpen()); - Q_ASSERT(!file->isSequential()); // skipping in sequential streams not currently needed - - char header[protocol::Message::HEADER_LEN]; - - if(file->read(header, protocol::Message::HEADER_LEN) != sizeof(protocol::Message::HEADER_LEN)) - return -1; - - const int payloadLen = qFromBigEndian(reinterpret_cast(header)); - - const qint64 target = file->pos() + payloadLen; - if(target > file->size() || !file->seek(target)) - return -1; - - if(msgType) - *msgType = header[2]; - if(ctxId) - *ctxId = header[3]; - - return protocol::Message::HEADER_LEN + payloadLen; -} - -} - diff --git a/src/libshared/record/header.h b/src/libshared/record/header.h deleted file mode 100644 index 6721652897..0000000000 --- a/src/libshared/record/header.h +++ /dev/null @@ -1,63 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -#ifndef DP_REC_HEADER_H -#define DP_REC_HEADER_H - -#include - -class QIODevice; -class QByteArray; -class QJsonObject; - -namespace recording { - -/** - * @brief Read a recording header - * - * A null object is returned if the header couldn't be read - * or the file is not a valid Drawpile recording. - * @param file - * @return header metadata block - */ -QJsonObject readRecordingHeader(QIODevice *file); - -/** - * @brief Write a recording header - * - * The keys "version" and "writerversion" will be automatically - * set if not present in the given metadata. - * - * @param file - * @param metadata header metadata - * @return false on IO error - */ -bool writeRecordingHeader(QIODevice *file, const QJsonObject &metadata); - -/** - * @brief Write the text mode recording header - */ -bool writeTextHeader(QIODevice *file, const QJsonObject &metadata); -/** - * @brief Read a single complete message from a file to the buffer - * - * The target buffer is resized to fit the message if necessary. - * - * @param file - * @param buffer - * @return false on IO error - */ -bool readRecordingMessage(QIODevice *file, QByteArray &buffer); - -/** - * @brief Skip a single message in the recording - * @param file - * @param msgType - * @param ctxId - * @return length of the message skipped or -1 if end whole message is not in the file - */ -int skipRecordingMessage(QIODevice *file, uint8_t *msgType=nullptr, uint8_t *ctxId=nullptr); - -} - -#endif - diff --git a/src/libshared/record/reader.cpp b/src/libshared/record/reader.cpp deleted file mode 100644 index ffc1a6d842..0000000000 --- a/src/libshared/record/reader.cpp +++ /dev/null @@ -1,461 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -#include "libshared/record/reader.h" -#include "libshared/record/header.h" -#include "libshared/net/recording.h" -#include "libshared/net/textmode.h" - -#include -#include -#include -#include - -namespace recording { - -using protocol::text::Parser; - -struct Reader::Private { - Encoding encoding; - QString filename; - QIODevice *file; - - QByteArray msgbuf; - - QJsonObject metadata; - - int current; - qint64 currentPos; - qint64 beginning; - - bool autoclose; - bool eof; - bool isCompressed; - bool opaque; -}; - -bool Reader::isRecordingExtension(const QString &filename) -{ - QRegularExpression re("\\.dp(?:rec|txt)(?:z|\\.(?:gz|bz2|xz))?$"); - return re.match(filename).hasMatch(); -} - -Reader::Reader(const QString &filename, Encoding encoding, QObject *parent) - : QObject(parent), d(new Private) -{ - d->encoding = encoding; - d->filename = filename; - d->current = -1; - d->currentPos = 0; - d->autoclose = true; - d->eof = false; - d->opaque = false; - d->file = new QFile(filename); -} - -Reader::Reader(const QString &filename, QIODevice *file, bool autoclose, Encoding encoding, QObject *parent) - : QObject(parent), d(new Private) -{ - Q_ASSERT(file); - d->encoding = encoding; - d->filename = filename; - d->file = file; - d->current = -1; - d->autoclose = autoclose; - d->eof = false; - d->isCompressed = false; -} - -Reader::~Reader() -{ - if(d->autoclose) - delete d->file; - delete d; -} - -bool Reader::isEof() const -{ - return d->eof; -} - -protocol::ProtocolVersion Reader::formatVersion() const -{ - return protocol::ProtocolVersion::fromString(d->metadata["version"].toString()); -} - -QJsonObject Reader::metadata() const -{ - return d->metadata; -} - -Reader::Encoding Reader::encoding() const -{ - return d->encoding; -} - -QString Reader::filename() const -{ - return d->filename; -} - -int Reader::currentIndex() const -{ - return d->current; -} - -qint64 Reader::currentPosition() const -{ - return d->currentPos; -} - -static Reader::Encoding detectEncoding(QIODevice *dev) -{ - // First, see if the binary header is present - QJsonObject header = readRecordingHeader(dev); - if(!header.isEmpty()) { - // Header content read! This must be a binary recording - return Reader::Encoding::Binary; - } - - dev->seek(0); - - // Check if this looks like a text mode recording - dev->setTextModeEnabled(true); - Parser parser; - for(int i=0;i<100;++i) { - QByteArray rawLine = dev->readLine(1024); - if(rawLine.isEmpty()) { - // Ran out of stuff, certainly not a valid recording - break; - } - - QString line = QString::fromUtf8(rawLine).trimmed(); - Parser::Result res = parser.parseLine(line); - switch(res.status) { - case Parser::Result::Ok: - case Parser::Result::NeedMore: - // Got a valid command: this really looks like a valid recording - return Reader::Encoding::Text; - break; - case Parser::Result::Skip: - // Hmm. Inconclusive, unless a metadata variable was set - if(!parser.metadata().isEmpty()) - return Reader::Encoding::Text; - break; - case Parser::Result::Error: - // Error encountered: most likely not a recording - return Reader::Encoding::Autodetect; - } - } - // No valid messages after 100 lines? Probably not a recording. - return Reader::Encoding::Autodetect; -} - - -Compatibility Reader::open() -{ - return open(false); -} - -Compatibility Reader::openOpaque() -{ - return open(true); -} - -Compatibility Reader::open(bool opaque) -{ - if(!d->file->isOpen()) { - if(!d->file->open(QFile::ReadOnly)) { - return CANNOT_READ; - } - } - - if(d->encoding == Encoding::Autodetect) { - d->encoding = detectEncoding(d->file); - // Still couldn't figure out the encoding? - if(d->encoding == Encoding::Autodetect) - return NOT_DPREC; - - d->file->seek(0); - } - - d->opaque = opaque; - - if(d->encoding == Encoding::Binary) - return readBinaryHeader(); - else - return readTextHeader(); -} - -Compatibility Reader::readBinaryHeader() { - // Read the header - d->metadata = readRecordingHeader(d->file); - - if(d->metadata.isEmpty()) { - return NOT_DPREC; - } - - // Header completed! - d->beginning = d->file->pos(); - - // Check version numbers - const auto version = formatVersion(); - - // Best case is exact match. - const auto current = protocol::ProtocolVersion::current(); - - if(version == current) - return COMPATIBLE; - - if(d->opaque) { - // In opaque mode, it's enough that the server version matches - if(version.serverVersion() == current.serverVersion()) - return COMPATIBLE; - - } else { - // Different namespace means this recording is meant for some other program - if(version.ns() != current.ns()) - return NOT_DPREC; - - // Backwards compatible mode: - // TODO - - // Strict compatibility mode: - - // A recording made with a newer (major) version may contain unsupported commands. - if(current.majorVersion() < version.majorVersion()) - return UNKNOWN_COMPATIBILITY; - - // Versions older than 21.x are known to be incompatible - if(version.majorVersion() < 21) - return INCOMPATIBLE; - - // Different minor version: expect rendering differences - if(current.minorVersion() != version.minorVersion()) - return MINOR_INCOMPATIBILITY; - } - - // Other versions are not supported - return INCOMPATIBLE; -} - -Compatibility Reader::readTextHeader() -{ - // Read the file until the first command is found - Parser parser; - bool done=false; - qint64 pos=0; - while(!done) { - QByteArray rawLine = d->file->readLine(); - if(rawLine.isEmpty()) - return NOT_DPREC; - - QString line = QString::fromUtf8(rawLine).trimmed(); - Parser::Result res = parser.parseLine(line); - switch(res.status) { - case Parser::Result::Skip: - // Comments or metadata. Remember this potential start of the first real message - pos = filePosition(); - break; - - case Parser::Result::Error: - return NOT_DPREC; - - case Parser::Result::NeedMore: - case Parser::Result::Ok: - // First message found, meaning the header section (if there was any) is over - done = true; - break; - } - } - - // Go to the beginning of the first message - d->beginning = pos; - d->file->seek(pos); - - // Convert header metadata to JSON format - protocol::KwargsIterator header(parser.metadata()); - while(header.hasNext()) { - header.next(); - bool ok; - int number = header.value().toInt(&ok); - if(ok) - d->metadata[header.key()] = number; - else if(header.value() == "true" || header.value() == "false") - d->metadata[header.key()] = header.value() == "true"; - else - d->metadata[header.key()] = header.value(); - } - - // Check compatibility - const auto version = formatVersion(); - - if(!version.isValid()) { - // No version header given - return UNKNOWN_COMPATIBILITY; - } - - // Best case is exact match. - const auto current = protocol::ProtocolVersion::current(); - - if(version == current) - return COMPATIBLE; - - if(d->opaque) { - // In opaque mode, version must match exactly, except for the minor number - if(version.ns() == current.ns() && version.serverVersion() == current.serverVersion() && version.majorVersion() == current.majorVersion()) - return COMPATIBLE; - - } else { - // Different namespace means this recording is meant for some other program - if(version.ns() != current.ns()) - return NOT_DPREC; - - // Backwards compatible mode: - // TODO - - // Strict compatibilty mode: - // A recording made with a newer (major) version may contain unsupported commands. - if(current.majorVersion() < version.majorVersion()) - return UNKNOWN_COMPATIBILITY; - - // Versions older than 20.x are known to be incompatible - if(version.majorVersion() < 20) - return INCOMPATIBLE; - - // Different minor version: expect rendering differences - if(current.minorVersion() != version.minorVersion()) - return MINOR_INCOMPATIBILITY; - } - - // Other versions are not supported - return INCOMPATIBLE; -} - -QString Reader::errorString() const -{ - return d->file->errorString(); -} - -QString Reader::writerVersion() const -{ - return d->metadata["writerversion"].toString(); -} - -qint64 Reader::filesize() const -{ - return d->file->size(); -} - -qint64 Reader::filePosition() const -{ - return d->file->pos(); -} - -void Reader::close() -{ - Q_ASSERT(d->file->isOpen()); - d->file->close(); -} - -void Reader::rewind() -{ - d->file->seek(d->beginning); - d->current = -1; - d->currentPos = -1; - d->eof = false; -} - -void Reader::seekTo(int pos, qint64 position) -{ - d->current = pos; - d->currentPos = position; - d->file->seek(position); - d->eof = false; -} - -static protocol::NullableMessageRef readTextMessage(QIODevice *file, bool *eof) -{ - Parser parser; - while(1) { - QByteArray rawLine = file->readLine(); - if(rawLine.isEmpty()) { - *eof = true; - return nullptr; - } - - Parser::Result res = parser.parseLine(QString::fromUtf8(rawLine).trimmed()); - switch(res.status) { - case Parser::Result::Skip: - case Parser::Result::NeedMore: - break; - case Parser::Result::Error: - qWarning("Text mode recording error: %s", parser.errorString().toLocal8Bit().constData()); - return nullptr; - case Parser::Result::Ok: - return res.msg; - } - } -} - -bool Reader::readNextToBuffer(QByteArray &buffer) -{ - Q_ASSERT(d->encoding != Encoding::Autodetect); - - d->currentPos = filePosition(); - - if(d->encoding == Encoding::Binary) { - if(!readRecordingMessage(d->file, buffer)) { - d->eof = true; - return false; - } - - } else { - protocol::NullableMessageRef msg = readTextMessage(d->file, &d->eof); - if(msg.isNull()) - return false; - if(buffer.length() < msg->length()) - buffer.resize(msg->length()); - msg->serialize(buffer.data()); - } - - ++d->current; - - return true; -} - -MessageRecord Reader::readNext() -{ - Q_ASSERT(d->encoding != Encoding::Autodetect); - - if(d->encoding == Encoding::Binary) { - if(!readNextToBuffer(d->msgbuf)) - return MessageRecord::Eor(); - - protocol::NullableMessageRef message; - message = protocol::Message::deserialize(reinterpret_cast(d->msgbuf.constData()), d->msgbuf.length(), !d->opaque); - - if(message.isNull()) - return MessageRecord::Invalid( - protocol::Message::sniffLength(d->msgbuf.constData()), - protocol::MessageType(d->msgbuf.at(2)) - ); - else - return MessageRecord::Ok(message); - - } else { - d->currentPos = filePosition(); - protocol::NullableMessageRef message = readTextMessage(d->file, &d->eof); - if(!d->eof) { - if(message.isNull()) - return MessageRecord::Invalid(0, protocol::MSG_COMMAND); - - ++d->current; - return MessageRecord::Ok(message); - } - } - - return MessageRecord::Eor(); -} - -} - diff --git a/src/libshared/record/reader.h b/src/libshared/record/reader.h deleted file mode 100644 index 4de9f6847a..0000000000 --- a/src/libshared/record/reader.h +++ /dev/null @@ -1,192 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -#ifndef REC_READER_H -#define REC_READER_H - -#include "libshared/net/message.h" -#include "libshared/net/protover.h" - -#include -#include - -class QIODevice; - -namespace recording { - -enum Compatibility { - //! Recording is fully compatible with current version - COMPATIBLE, - //! Minor changes: file will load but may appear different - MINOR_INCOMPATIBILITY, - //! Might be at least partially compatible. Unknown future version - UNKNOWN_COMPATIBILITY, - //! File is a recording, but known to be incompatible - INCOMPATIBLE, - //! File is not a recording at all - NOT_DPREC, - //! Couldn't read file due to IO error - CANNOT_READ -}; - -struct MessageRecord { - static MessageRecord Ok(protocol::NullableMessageRef msg) { return MessageRecord { OK, msg, 0, protocol::MSG_COMMAND }; } - static MessageRecord Invalid(int len, protocol::MessageType type) { return MessageRecord { INVALID, nullptr, len, type }; } - static MessageRecord Eor() { return MessageRecord { END_OF_RECORDING, nullptr, 0, protocol::MSG_COMMAND }; } - - enum { OK, INVALID, END_OF_RECORDING } status; - - // The message (if status is OK) - protocol::NullableMessageRef message; - - // These are set if status is INVALID - int invalid_len; - protocol::MessageType invalid_type; -}; - -/** - * @brief Recording file reader - * - * Supports both binary and text encodings. - */ -class Reader final : public QObject -{ - Q_OBJECT -public: - enum class Encoding { - Autodetect, - Binary, - Text - }; - - /** - * @brief Read from a file - * - * Compression is handled transparently. - * - * @brief Reader - * @param filename - * @param parent - */ - explicit Reader(const QString &filename, Encoding encoding=Encoding::Autodetect, QObject *parent=nullptr); - - /** - * @brief Read from an IO device - * - * @param filename the original file name - * @param file input file device - * @param autoclose if true, the Reader instance will take ownership of the file device - * @param parent - */ - Reader(const QString &filename, QIODevice *file, bool autoclose=false, Encoding=Encoding::Autodetect, QObject *parent=nullptr); - - ~Reader() override; - - /** - * @brief Check if the given filename has a .dprec(+compression type) extension - * @param filename - * @return true if file is probably a recording - */ - static bool isRecordingExtension(const QString &filename); - - //! Name of the currently open file - QString filename() const; - - //! Size of the currently open file - qint64 filesize() const; - - //! Index of the last read message - int currentIndex() const; - - //! Position of the last read message in the file - qint64 currentPosition() const; - - //! Position in the file (position of the next message to be read) - qint64 filePosition() const; - - //! Did the last read hit the end of the file? - bool isEof() const; - - //! Get the last error message - QString errorString() const; - - //! Get the recording's protocol version - protocol::ProtocolVersion formatVersion() const; - - //! Get the version number of the program that made the recording - QString writerVersion() const; - - //! Get header metadata - QJsonObject metadata() const; - - /** - * @brief Open the file - * @return compatibility level of the opened file - */ - Compatibility open(); - - /** - * @brief Open the file in opaque reading mode - * - * In this mode, a binary file is either compatible or - * incompatible with no partial compatibility modes. - * All versions where the server version matches the - * reader's are considered fully compatible. - * OpaqueMessage class is used for all opaque message types. - * - * For text mode recordings, the version must match exactly, except - * for the minor number. - * Real message classes are returned for opaque messages. - */ - Compatibility openOpaque(); - - /** - * @brief Get the recording encoding mode - */ - Encoding encoding() const; - - //! Close the file - void close(); - - //! Rewind to the first message - void rewind(); - - /** - * @brief Read the next message to the given buffer - * - * The buffer will be resized, if necesasry, to hold the entire message. - * If this is a text mode recording, the message will be serialized in the buffer. - * - * @param buffer - * @return false on error - */ - bool readNextToBuffer(QByteArray &buffer); - - /** - * @brief Read the next message - * @return - */ - MessageRecord readNext(); - - /** - * @brief Seek to given position in the recording - * - * Calling this will reset the EOF flag. - * - * @param pos entry index - * @param offset entry offset in the file - */ - void seekTo(int pos, qint64 offset); - -private: - Compatibility open(bool opaque); - Compatibility readBinaryHeader(); - Compatibility readTextHeader(); - - struct Private; - Private *d; - -}; - -} - -#endif diff --git a/src/libshared/record/writer.cpp b/src/libshared/record/writer.cpp deleted file mode 100644 index d4ef5d7939..0000000000 --- a/src/libshared/record/writer.cpp +++ /dev/null @@ -1,215 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -#include "libshared/record/writer.h" -#include "libshared/record/header.h" -#include "libshared/net/recording.h" - -#include -#include -#include -#include - -#include - -namespace recording { - -Writer::Writer(const QString &filename, QObject *parent) - : Writer(new QFile(filename), true, parent) -{ - if(filename.contains(".dptxt", Qt::CaseInsensitive) && !filename.contains(".dprec", Qt::CaseInsensitive)) - m_encoding = Encoding::Text; -} - - -Writer::Writer(QIODevice *file, bool autoclose, QObject *parent) - : QObject(parent), m_file(file), - m_autoclose(autoclose), m_minInterval(0), m_timestampInterval(0), m_lastTimestamp(0), - m_autoflush(nullptr), m_encoding(Encoding::Binary) -{ -} - -Writer::~Writer() -{ - if(m_autoclose) - delete m_file; -} - -void Writer::setMinimumInterval(int min) -{ - m_minInterval = min; - m_interval = QDateTime::currentMSecsSinceEpoch(); -} - -void Writer::setTimestampInterval(int interval) -{ - m_timestampInterval = interval; -} - -void Writer::setAutoflush() -{ - if(m_autoflush != nullptr) - return; - - auto *fd = qobject_cast(m_file); - if(!fd) { - qWarning("Cannot enable recording autoflush: output device not a QFileDevice"); - return; - } - - m_autoflush = new QTimer(this); - m_autoflush->setSingleShot(false); - connect(m_autoflush, &QTimer::timeout, fd, &QFileDevice::flush); - m_autoflush->start(5000); -} - -void Writer::setEncoding(Encoding e) -{ - Q_ASSERT(m_file->pos()==0); - m_encoding = e; -} - -bool Writer::open() -{ - if(m_file->isOpen()) - return true; - - return m_file->open(QIODevice::WriteOnly); -} - -QString Writer::errorString() const -{ - return m_file->errorString(); -} - -bool Writer::writeHeader(const QJsonObject &customMetadata) -{ - if(m_encoding == Encoding::Binary) - return writeRecordingHeader(m_file, customMetadata); - else - return writeTextHeader(m_file, customMetadata); -} - -void Writer::writeFromBuffer(const QByteArray &buffer) -{ - if(m_encoding == Encoding::Binary) { - const int len = protocol::Message::sniffLength(buffer.constData()); - Q_ASSERT(len <= buffer.length()); - m_file->write(buffer.constData(), len); - - } else { - protocol::NullableMessageRef msg = protocol::Message::deserialize(reinterpret_cast(buffer.constData()), buffer.length(), true); - m_file->write(msg->toString().toUtf8()); - m_file->write("\n", 1); - } -} - -bool Writer::writeMessage(const protocol::Message &msg) -{ - Q_ASSERT(m_file->isOpen()); - - if(m_encoding == Encoding::Binary) { - QVarLengthArray buf(msg.length()); - const int len = msg.serialize(buf.data()); - Q_ASSERT(len == buf.length()); - if(m_file->write(buf.data(), len) != len) - return false; - - } else { - if(msg.type() == protocol::MSG_FILTERED) { - // Special case: Filtered messages are - // written as comments in the text format. - const protocol::Filtered &fm = static_cast(msg); - auto wrapped = fm.decodeWrapped(); - - QString comment; - if(wrapped.isNull()) { - comment = QStringLiteral("FILTERED: undecodable message type #%1 of length %2") - .arg(fm.wrappedType()) - .arg(fm.wrappedPayloadLength()); - - } else { - comment = QStringLiteral("FILTERED: ") + wrapped->toString(); - } - - return writeComment(comment); - } - - QByteArray line = msg.toString().toUtf8(); - if(m_file->write(line) != line.length()) - return false; - if(m_file->write("\n", 1) != 1) - return false; - - // Write extra newlines after certain commands to give - // the file some visual structure - switch(msg.type()) { - case protocol::MSG_UNDOPOINT: - if(m_file->write("\n", 1) != 1) - return false; - break; - default: break; - } - } - - return true; -} - -bool Writer::writeComment(const QString &comment) -{ - if(m_encoding != Encoding::Text) - return true; - - QList lines = comment.toUtf8().split('\n'); - for(const QByteArray &line : lines) { - if(m_file->write("# ", 2) != 2) - return false; - - if(m_file->write(line) != line.length()) - return false; - - if(m_file->write("\n", 1) != 1) - return false; - } - - return true; -} - -void Writer::recordMessage(const protocol::MessagePtr &msg) -{ - if(msg->isRecordable()) { - const qint64 now = QDateTime::currentMSecsSinceEpoch(); - - if(m_minInterval>0) { - const qint64 interval = now - m_interval; - if(interval >= m_minInterval) { - writeMessage(protocol::Interval(0, qMin(qint64(0xffff), interval))); - } - m_interval = now; - } - - if(m_timestampInterval > 0) { - const qint64 interval = now - m_lastTimestamp; - if(interval >= m_timestampInterval) { - writeMessage(protocol::Marker(0, QDateTime::currentDateTime().toString())); - m_lastTimestamp = now; - } - } - - writeMessage(*msg); - } -} - -void Writer::close() -{ - if(m_autoflush) { - m_autoflush->stop(); - delete m_autoflush; - m_autoflush = nullptr; - } - - if(m_file->isOpen()) - m_file->close(); -} - -} - diff --git a/src/libshared/record/writer.h b/src/libshared/record/writer.h deleted file mode 100644 index fc519357ac..0000000000 --- a/src/libshared/record/writer.h +++ /dev/null @@ -1,136 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -#ifndef WRITER_H -#define WRITER_H - -#include "libshared/net/message.h" - -#include -#include - -class QIODevice; -class QTimer; - -namespace recording { - -class Writer final : public QObject -{ - Q_OBJECT -public: - enum class Encoding { - Binary, - Text - }; - - /** - * @brief Open a writer that writes to the named file - * - * If the file ends with ".dptxt", the text encoding is used. - * - * @param filename - * @param parent - */ - Writer(const QString &filename, QObject *parent=nullptr); - - /** - * @brief Open a writer that writes to the given device - * @param file file device - * @param autoclose if true, this object will take ownership of the file device - * @param parent - */ - Writer(QIODevice *file, bool autoclose=false, QObject *parent=nullptr); - ~Writer() override; - - QString errorString() const; - - /** - * @brief Select the recording encoding to use. - * - * This must be called before any write operation. - */ - void setEncoding(Encoding e); - - //! Open the file for writing - bool open(); - - //! Close the file - void close(); - - //! Enable periodic flushing of the output file - void setAutoflush(); - - /** - * @brief Set the minimum time between messages before writing an Interval message - * - * Set the time to 0 to disable interval messages altogether - * - * @param min minimum time in milliseconds (0 to disable) - */ - void setMinimumInterval(int min); - - /** - * @brief Set the interval between timestamp markers. - * If set to zero, timestamps are not recorded. - * @param interval in milliseconds (0 to disable) - */ - void setTimestampInterval(int interval); - - /** - * @brief Write recording header - * - * This should be called before writing the first message. - * - * Custom metadata can be included. If no "version" field is set, - * the current protocol version will be used. - * - * @return false on error - */ - bool writeHeader(const QJsonObject &customMetadata=QJsonObject()); - - /** - * @brief Write a message from a buffer - * - * Note. The buffer must contain a valid serialized Message! - * @param buffer - */ - void writeFromBuffer(const QByteArray &buffer); - - /** - * @brief Write a message - * @return false on error - */ - bool writeMessage(const protocol::Message &msg); - - /** - * @brief Write a comment line - * - * If encoding is not Text, this does nothing. - * - * @return false on error - */ - bool writeComment(const QString &comment); - -public slots: - /** - * @brief Record a message - * - * This writes the message to the recording only if the type is recordable. - * If a minimum interval is set and enough time has passed - * since the last recordMessage call, an Interval message is writen as well. - */ - void recordMessage(const protocol::MessagePtr &msg); - -private: - QIODevice *m_file; - bool m_autoclose; - qint64 m_minInterval; - qint64 m_interval; - qint64 m_timestampInterval; - qint64 m_lastTimestamp; - QTimer *m_autoflush; - Encoding m_encoding; -}; - -} - -#endif diff --git a/src/libshared/tests/CMakeLists.txt b/src/libshared/tests/CMakeLists.txt index 1112da4487..4c5b723a6c 100644 --- a/src/libshared/tests/CMakeLists.txt +++ b/src/libshared/tests/CMakeLists.txt @@ -2,7 +2,7 @@ find_package(${QT_PACKAGE_NAME} ${DP_MIN_QT_VERSION_SERVER} REQUIRED COMPONENTS add_unit_tests(shared LIBS dpshared ${QT_PACKAGE_NAME}::Test - TESTS passwordhash filename messages recording messagequeue listings ulid + TESTS passwordhash filename messagequeue listings ulid ) if(TARGET libsodium::libsodium) diff --git a/src/libshared/tests/messagequeue.cpp b/src/libshared/tests/messagequeue.cpp index 5a03fccc32..a4168bd2ff 100644 --- a/src/libshared/tests/messagequeue.cpp +++ b/src/libshared/tests/messagequeue.cpp @@ -1,7 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later #include "libshared/net/messagequeue.h" -#include "libshared/net/meta.h" #include #include @@ -12,8 +11,6 @@ #include #include -using namespace protocol; - // A simple TCP server that echoes back whatever is written to it. // Used by the actual test case. class EchoServer final : public QObject @@ -126,14 +123,17 @@ private slots: { auto mq = getMsgQueue(); - MessagePtr msg(new Chat(0, 0, 0, QByteArray("Hello world!"))); + net::Message msg = + net::makeChatMessage(0, 0, 0, QStringLiteral("Hello world!")); bool messageReceived = false; - connect(mq.get(), &MessageQueue::messageAvailable, [&mq, msg, &messageReceived]() { - MessagePtr got = mq->getPending(); + connect(mq.get(), &net::MessageQueue::messageAvailable, [&mq, msg, &messageReceived]() { + net::MessageList got; + mq->receive(got); messageReceived = true; - QVERIFY(got.equals(msg)); + QVERIFY(got.size() == 1); + QVERIFY(got[0].equals(msg)); }); mq->send(msg); loopUntil(messageReceived); @@ -148,22 +148,29 @@ private slots: int countReceived = 0; bool allReceived = false; - connect(mq.get(), &MessageQueue::messageAvailable, [&]() { + connect(mq.get(), &net::MessageQueue::messageAvailable, [&]() { while(mq->isPending()) { - MessagePtr got = mq->getPending(); + net::MessageList got; + mq->receive(got); // Expect to receive the messages in the same order as we sent them - QCOMPARE(got->type(), MSG_CHAT); - QCOMPARE(got.cast().message(), QString::number(countReceived)); - if(++countReceived == sendCount) - allReceived = true; + for(const net::Message &msg : got) { + QCOMPARE(msg.type(), DP_MSG_CHAT); + size_t len; + const char *text = DP_msg_chat_message(msg.toChat(), &len); + QCOMPARE(QString::fromUtf8(text, len), QString::number(countReceived)); + if(++countReceived == sendCount) { + allReceived = true; + } + } QVERIFY(countReceived <= sendCount); } }); int totalSendLen = 0; for(int i=0;ilength(); + net::Message msg = + net::makeChatMessage(0, 0, 0, QByteArray::number(i)); + totalSendLen += msg.length(); mq->send(msg); } @@ -176,7 +183,7 @@ private slots: void testSendDisconnect() { auto s = getConnection(); - MessageQueue mq(s.get()); + net::MessageQueue mq(s.get(), true, nullptr); bool disconnected = false; @@ -186,7 +193,7 @@ private slots: // Note: because the connection is cut just after the disconnect message // is sent, we can't test the reception of the message with an echo server. - mq.sendDisconnect(0, "test"); + mq.sendDisconnect(net::MessageQueue::GracefulDisconnect(0), "test"); loopUntil(disconnected); } @@ -199,10 +206,10 @@ private slots: return s; } - std::unique_ptr getMsgQueue() + std::unique_ptr getMsgQueue() { auto s = getConnection(); - std::unique_ptr q { new MessageQueue(s.get()) }; + std::unique_ptr q { new net::MessageQueue(s.get(), true, nullptr) }; s->setParent(q.get()); s.release(); return q; diff --git a/src/libshared/tests/messages.cpp b/src/libshared/tests/messages.cpp deleted file mode 100644 index 88d595e65d..0000000000 --- a/src/libshared/tests/messages.cpp +++ /dev/null @@ -1,160 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -#include "libshared/net/annotation.h" -#include "libshared/net/control.h" -#include "libshared/net/meta.h" -#include "libshared/net/meta2.h" -#include "libshared/net/recording.h" -#include "libshared/net/layer.h" -#include "libshared/net/image.h" -#include "libshared/net/undo.h" -#include "libshared/net/brushes.h" -#include "libshared/net/textmode.h" - -#include - -using namespace protocol; - -Q_DECLARE_METATYPE(NullableMessageRef) - -typedef QList IdList; - -class TestMessages final : public QObject -{ - Q_OBJECT -private slots: - void testMessageSerialization_data() - { - QTest::addColumn("msg"); - QTest::newRow("command") << NullableMessageRef{static_cast(new Command(0, QByteArray("testing...")))}; - QTest::newRow("disconnect") << NullableMessageRef{static_cast(new Disconnect(1, Disconnect::KICK, "hello"))}; - QTest::newRow("ping") << NullableMessageRef{static_cast(new Ping(2, true))}; - - QTest::newRow("userjoin") << NullableMessageRef{static_cast(new UserJoin(4, 0x03, QString("Test"), "asd"))}; - QTest::newRow("userjoin(no hash)") << NullableMessageRef{static_cast(new UserJoin(4, 0x03, QString("Test"), QByteArray()))}; - QTest::newRow("userleave") << NullableMessageRef{static_cast(new UserLeave(5))}; - QTest::newRow("sessionowner") << NullableMessageRef{static_cast(new SessionOwner(6, QList() << 1 << 2 << 5))}; - QTest::newRow("softreset") << NullableMessageRef{static_cast(new SoftResetPoint(60))}; - QTest::newRow("chat") << NullableMessageRef{static_cast(new Chat(7, 0x01, 0x04, QByteArray("Test")))}; - - QTest::newRow("interval") << NullableMessageRef{static_cast(new Interval(8, 0x1020))}; - QTest::newRow("lasertrail") << NullableMessageRef{static_cast(new LaserTrail(9, 0xff223344, 0x80))}; - QTest::newRow("movepointer") << NullableMessageRef{static_cast(new MovePointer(10, 0x11223344, 0x55667788))}; - QTest::newRow("marker") << NullableMessageRef{static_cast(new Marker(11, QString("Test")))}; - QTest::newRow("useracl") << NullableMessageRef{static_cast(new UserACL(12, QList() << 1 << 2 << 4))}; - QTest::newRow("layeracl") << NullableMessageRef{static_cast(new LayerACL(13, 0x1122, 0x01, 0x02, QList() << 3 << 4 << 5))}; - QTest::newRow("featureaccess") << NullableMessageRef{static_cast(new FeatureAccessLevels(14, reinterpret_cast("\0\1\2\3\0\1\2\3\0")))}; - QTest::newRow("defaultlayer") << NullableMessageRef{static_cast(new DefaultLayer(14, 0x1401))}; - - QTest::newRow("undopoint") << NullableMessageRef{static_cast(new UndoPoint(15))}; - QTest::newRow("canvasresize") << NullableMessageRef{static_cast(new CanvasResize(16, -0xfff, 0xaaa, -0xbbb, 0xccc))}; - QTest::newRow("background(color)") << NullableMessageRef{static_cast(new CanvasBackground(17, 0x00ff0000))}; - QTest::newRow("background(img)") << NullableMessageRef{static_cast(new CanvasBackground(17, QByteArray(64*64*4, '\xff')))}; - QTest::newRow("layercreate") << NullableMessageRef{static_cast(new LayerCreate(17, 0xaabb, 0xccdd, 0x11223344, 0x01, QString("Test layer")))}; - QTest::newRow("layerattributes") << NullableMessageRef{static_cast(new LayerAttributes(18, 0xaabb, 0xcc, LayerAttributes::FLAG_CENSOR, 0x10, 0x22))}; - QTest::newRow("layerretitle") << NullableMessageRef{static_cast(new LayerRetitle(19, 0xaabb, QString("Test")))}; - QTest::newRow("layerorder") << NullableMessageRef{static_cast(new LayerOrder(20, QList() << 0x1122 << 0x3344 << 0x4455))}; - QTest::newRow("layervisibility") << NullableMessageRef{static_cast(new LayerVisibility(21, 0x1122, 1))}; - QTest::newRow("putimage") << NullableMessageRef{static_cast(new PutImage(22, 0x1122, 0x10, 100, 200, 300, 400, QByteArray("Test")))}; - QTest::newRow("puttile") << NullableMessageRef{static_cast(new PutTile(22, 0x1122, 0x10, 1, 2, 3, 0xaabbccdd))}; - QTest::newRow("fillrect") << NullableMessageRef{static_cast(new FillRect(23, 0x1122, 0x10, 3, 200, 300, 400, 0x11223344))}; - QTest::newRow("penup") << NullableMessageRef{static_cast(new PenUp(26))}; - QTest::newRow("annotationcreate") << NullableMessageRef{static_cast(new AnnotationCreate(27, 0x1122, -100, -100, 200, 200))}; - QTest::newRow("annotationreshape") << NullableMessageRef{static_cast(new AnnotationReshape(28, 0x1122, -100, -100, 200, 200))}; - QTest::newRow("annotationedit") << NullableMessageRef{static_cast(new AnnotationEdit(29, 0x1122, 0x12345678, 7, 0x0a, QByteArray("Test")))}; - QTest::newRow("annotationdelete") << NullableMessageRef{static_cast(new AnnotationDelete(30, 0x1122))}; - QTest::newRow("moveregion") << NullableMessageRef{static_cast(new MoveRegion(30, 0x1122, 0, 1, 2, 3, 10, 11, 20, 21, 30, 31, 40, 41, QByteArray("test")))}; - - QTest::newRow("classicdabs") << NullableMessageRef{static_cast(new DrawDabsClassic(31, 0x1122, 100, -100, 0xff223344, 0x10, ClassicBrushDabVector() << ClassicBrushDab {1, 2, 3, 4, 5} << ClassicBrushDab {10, 20, 30, 40, 50}))}; - QTest::newRow("pixeldabs") << NullableMessageRef{static_cast(new DrawDabsPixel(DabShape::Round, 32, 0x1122, 100, -100, 0xff223344, 0x10, PixelBrushDabVector() << PixelBrushDab {1, 2, 3, 4} << PixelBrushDab {10, 20, 30, 40}))}; - QTest::newRow("squarepixeldabs") << NullableMessageRef{static_cast(new DrawDabsPixel(DabShape::Square, 32, 0x1122, 100, -100, 0xff223344, 0x10, PixelBrushDabVector() << PixelBrushDab {1, 2, 3, 4} << PixelBrushDab {10, 20, 30, 40}))}; - - QTest::newRow("undo") << NullableMessageRef{static_cast(new Undo(254, 1, false))}; - QTest::newRow("redo") << NullableMessageRef{static_cast(new Undo(254, 1, true))}; - } - - void testMessageSerialization() - { - QFETCH(NullableMessageRef, msg); - - // Test binary serialization - auto notEqual = Command(1, QByteArray("testing...")); - QVERIFY(!msg->equals(notEqual)); - QVERIFY(msg->equals(*msg)); - - QByteArray buffer(msg->length(), 0); - QCOMPARE(msg->serialize(buffer.data()), msg->length()); - - NullableMessageRef msg2 = Message::deserialize(reinterpret_cast(buffer.constData()), buffer.size(), true); - QVERIFY(!msg2.isNull()); - - QVERIFY(msg->equals(*msg2)); - - // Test text serialization (only valid for recordable types) - if(msg->isRecordable()) { - QStringList text = msg->toString().split('\n'); - - text::Parser parser; - text::Parser::Result r { text::Parser::Result::NeedMore, nullptr }; - for(const QString &line : text) { - QCOMPARE(r.status, text::Parser::Result::NeedMore); - r = parser.parseLine(line.trimmed()); - if(r.status == text::Parser::Result::Error) - QFAIL(parser.errorString().toLocal8Bit().constData()); - }; - QCOMPARE(r.status, text::Parser::Result::Ok); - QVERIFY(!r.msg.isNull()); - QVERIFY(msg->equals(*r.msg)); - } - } - - void testFilteredWrapping() - { - MessagePtr original = MessagePtr(new CanvasResize(1, 2, 3, 4, 5)); - MessagePtr filtered = original->asFiltered(); - - // Serializing filtered messages should work - QByteArray serialized(filtered->length(), 0); - const int written = filtered->serialize(serialized.data()); - QCOMPARE(written, filtered->length()); - - // As should deserializing - NullableMessageRef deserialized = Message::deserialize(reinterpret_cast(serialized.data()), serialized.length(), true); - QVERIFY(!deserialized.isNull()); - QCOMPARE(deserialized->type(), MSG_FILTERED); - - // The wrapped message should stay intact through the process - // (assuming payload length is less than 65535) - NullableMessageRef unwrapped = deserialized.cast().decodeWrapped(); - QVERIFY(!unwrapped.isNull()); - QVERIFY(unwrapped->equals(*original)); - } - - void testLayerOrderSanitation_data() - { - QTest::addColumn("reorder"); - QTest::addColumn("expected"); - - QTest::newRow("valid") << (IdList() << 4 << 3 << 1 << 2) << (IdList() << 4 << 3 << 1 << 2); - QTest::newRow("missing") << (IdList() << 3 << 1 << 2) << (IdList() << 3 << 1 << 2 << 4); - QTest::newRow("extra") << (IdList() << 5 << 4 << 3 << 2 << 1) << (IdList() << 4 << 3 << 2 << 1); - QTest::newRow("doubles") << (IdList() << 4 << 4 << 3 << 2 << 1) << (IdList() << 4 << 3 << 2 << 1); - QTest::newRow("empty") << (IdList()) << (IdList() << 1 << 2 << 3 << 4); - } - - void testLayerOrderSanitation() - { - QFETCH(IdList, reorder); - QFETCH(IdList, expected); - - QList current; - current << 1 << 2 << 3 << 4; - - QCOMPARE(LayerOrder(1, reorder).sanitizedOrder(current), expected); - } -}; - - -QTEST_MAIN(TestMessages) -#include "messages.moc" - diff --git a/src/libshared/tests/recording.cpp b/src/libshared/tests/recording.cpp deleted file mode 100644 index 57d8632764..0000000000 --- a/src/libshared/tests/recording.cpp +++ /dev/null @@ -1,233 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -#include "libshared/record/reader.h" -#include "libshared/record/writer.h" -#include "libshared/record/header.h" - -#include "libshared/net/control.h" -#include "libshared/net/meta.h" - -#include -#include -#include -#include - -#include - -using namespace recording; -using namespace protocol; - -// Hex encoded test recording. -// Header contains one extra key: "test": "TESTING" -// Protocol version is "dp:4.24.0" -// Body contains one message: UserJoin(1, 0, "hello", "world") -static const char *TEST_RECORDING = "44505245430000427b2274657374223a2254455354494e47222c2276657273696f6e223a2264703a342e32342e30222c2277726974657276657273696f6e223a22322e302e306232227d000c2001000568656c6c6f776f726c64"; - -// A test recording with a version number of dp:4.10.0, containing a single NewLayer message. -static const char *TEST_RECORDING_OLD = "44505245430000317b2276657273696f6e223a2264703a342e31302e30222c2277726974657276657273696f6e223a22322e302e306232227d00098201000100000000000000"; - -static const char *TEST_TEXTMODE = - "!version=dp:4.24.0\n" - "!test=TESTING\n" - "1 join name=hello avatar=d29ybGQ=\n"; - -class TestRecording final : public QObject -{ - Q_OBJECT -private slots: - void testWriter() - { - QBuffer buffer; - buffer.open(QBuffer::ReadWrite); - - MessagePtr testMsg(new UserJoin(1, 0, QByteArray("hello"), QByteArray("world"))); - { - Writer writer(&buffer, false); - QJsonObject header; - header["test"] = "TESTING"; - writer.writeHeader(header); - - // Non-recordable message: this should not be written - writer.recordMessage(MessagePtr(new Command(0, QByteArray("{}")))); - - // This should be written - writer.recordMessage(testMsg); - } - - // Autoclose is off: writer shouldn't have closed the IO device - QVERIFY(buffer.isOpen()); - - // Verify that the format looks like expected - const QByteArray b = buffer.buffer(); - - // File should start with the magic number - QVERIFY(b.startsWith(QByteArray("DPREC", 6))); - - // Followed by the JSON encoded metadata block - quint16 mdlen = qFromBigEndian(reinterpret_cast(b.constData())+6); - QJsonDocument mddoc = QJsonDocument::fromJson(b.mid(8, mdlen)); - - QVERIFY(mddoc.isObject()); - - QJsonObject mdobj = mddoc.object(); - - // The protocol version should be automatically included - QCOMPARE(mdobj["version"].toString(), ProtocolVersion::current().asString()); - - // The extra header we added should be there too - QCOMPARE(mdobj["test"].toString(), QString("TESTING")); - - // After the header, the recording should contain just the one recordable message - QCOMPARE(b.length() - 8 - mdlen, testMsg->length()); - - QByteArray msgbuf(testMsg->length(), 0); - testMsg->serialize(msgbuf.data()); - - QCOMPARE(msgbuf, b.mid(8+mdlen)); - } - - void testReader_data() { - QTest::addColumn("testRecording"); - QTest::addColumn("encoding"); - QTest::newRow("bin") << QByteArray::fromHex(TEST_RECORDING) << int(Reader::Encoding::Binary); - QTest::newRow("text") << QByteArray(TEST_TEXTMODE) << int(Reader::Encoding::Text); - } - - void testReader() - { - QFETCH(QByteArray, testRecording); - QFETCH(int, encoding); - QBuffer buffer(&testRecording); - buffer.open(QBuffer::ReadOnly); - - { - Reader reader("test", &buffer, false); - QCOMPARE(reader.filesize(), testRecording.length()); - - Compatibility compat = reader.open(); - QCOMPARE(reader.formatVersion().asString(), QString("dp:4.24.0")); - QCOMPARE(compat, COMPATIBLE); - QCOMPARE(int(reader.encoding()), encoding); - QCOMPARE(reader.metadata()["test"].toString(), QString("TESTING")); - - // No message read yet - QCOMPARE(reader.currentIndex(), -1); - const qint64 firstPosition = reader.filePosition(); - - // There should be exactly one message in the test recording - const MessageRecord mr1 = reader.readNext(); - - QCOMPARE(mr1.status, MessageRecord::OK); - - MessagePtr testMsg(new UserJoin(1, 0, QByteArray("hello"), QByteArray("world"))); - - QVERIFY(mr1.message.equals(testMsg)); - - // current* returns the index and position of the last read message - QCOMPARE(reader.currentIndex(), 0); - QCOMPARE(reader.currentPosition(), firstPosition); - - // Next message should be EOF - const MessageRecord mr2 = reader.readNext(); - QCOMPARE(mr2.status, MessageRecord::END_OF_RECORDING); - QVERIFY(reader.isEof()); - - // Rewinding should take us back to the beginning - reader.rewind(); - QCOMPARE(reader.currentIndex(), -1); - QCOMPARE(reader.filePosition(), firstPosition); - - const MessageRecord mr3 = reader.readNext(); - QCOMPARE(mr3.status, MessageRecord::OK); - QVERIFY(mr1.message.equals(mr3.message)); - } - - // Autoclose is not enabled - QVERIFY(buffer.isOpen()); - } - - void testSkip() - { - QBuffer buffer; - buffer.open(QBuffer::ReadWrite); - - MessagePtr testMsg(new UserJoin(1, 0, QByteArray("hello"), QByteArray("world"))); - Writer writer(&buffer, false); - writer.writeMessage(*testMsg); - writer.writeMessage(*testMsg); - - buffer.seek(0); - uint8_t mtype, ctx; - QCOMPARE(skipRecordingMessage(&buffer, &mtype, &ctx), testMsg->length()); - QCOMPARE(buffer.pos(), testMsg->length()); - QCOMPARE(mtype, uint8_t(MSG_USER_JOIN)); - QCOMPARE(ctx, uint8_t(1)); - - QCOMPARE(skipRecordingMessage(&buffer), testMsg->length()); - QCOMPARE(buffer.pos(), buffer.size()); - - QVERIFY(skipRecordingMessage(&buffer)<0); - } - - void testVersionMismatch() - { - QByteArray testRecording = QByteArray::fromHex(TEST_RECORDING_OLD); - QBuffer buffer(&testRecording); - buffer.open(QBuffer::ReadOnly); - - Reader reader("test", &buffer, false); - - Compatibility compat = reader.open(); - QCOMPARE(compat, INCOMPATIBLE); - } - - void testTextVersionMismatch_data() { - QByteArray data("1 join name=hello hash=world\n"); - QTest::addColumn("testRecording"); - QTest::addColumn("compat"); - QTest::newRow("ok") << "!version=" + protocol::ProtocolVersion::current().asString().toUtf8() + "\n" + data << true; - QTest::newRow("wrongMajor") << "!version=dp:4.10.0\n" + data << false; - QTest::newRow("wrongServer") << "!version=dp:3.20.0\n" + data << false; - QTest::newRow("wrongNs") << "!version=pd:4.20.0\n" + data << false; - } - - void testTextVersionMismatch() - { - QFETCH(QByteArray, testRecording); - QFETCH(bool, compat); - QBuffer buffer(&testRecording); - buffer.open(QBuffer::ReadOnly); - - // In opaque mode, version must match exactly (except for the minor number) - Reader reader("test", &buffer, false); - Compatibility c = reader.openOpaque(); - int expectedCompat = compat ? int(COMPATIBLE) : int(INCOMPATIBLE); - QCOMPARE(int(c), expectedCompat); - } - - void testOpaqueBinary() - { - QByteArray testRecording = QByteArray::fromHex(TEST_RECORDING_OLD); - QBuffer buffer(&testRecording); - buffer.open(QBuffer::ReadOnly); - - Reader reader("test", &buffer, false); - - // In opaque mode, any binary recording with a matching server number is compatible - Compatibility compat = reader.openOpaque(); - QCOMPARE(compat, COMPATIBLE); - - QCOMPARE(reader.formatVersion().asString(), QString("dp:4.10.0")); - - // The actual message should be of type OpaqueMessage - MessageRecord mr = reader.readNext(); - QCOMPARE(mr.status, MessageRecord::OK); - QVERIFY(!mr.message.isNull()); - QCOMPARE(mr.message->type(), protocol::MSG_LAYER_CREATE); - } -}; - - -QTEST_MAIN(TestRecording) -#include "recording.moc" - diff --git a/src/thinsrv/CMakeLists.txt b/src/thinsrv/CMakeLists.txt index dd9cf23329..02407dcd60 100644 --- a/src/thinsrv/CMakeLists.txt +++ b/src/thinsrv/CMakeLists.txt @@ -41,6 +41,25 @@ target_link_libraries(${srvlib} PUBLIC ${QT_PACKAGE_NAME}::Sql ) +# For some reason there's circular dependency issues here. Solve them with a +# link group, which unfortunately requires hacks on old CMake. +if(${CMAKE_VERSION} VERSION_LESS "3.24.0") + message(WARNING + "Your CMake version ${CMAKE_VERSION} is older than 3.24.0, link groups " + "are not available. Will try to make it work anyway, knock on wood.") + if(CMAKE_CXX_COMPILER_ID MATCHES "GNU" OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") + # This ends up grouping too many libraries, but otherwise seems to work. + # Better than refusing operation on old cmake versions I guess. + target_link_libraries(${srvlib} PUBLIC + -Wl,--start-group dpengine dpmsg dpmsg_rust -Wl,--end-group + ) + endif() +else() + target_link_libraries(${srvlib} PUBLIC + "$" + ) +endif() + target_link_libraries(${srvname} PRIVATE ${srvlib}) if(SERVERGUI) diff --git a/src/thinsrv/headless/headless.cpp b/src/thinsrv/headless/headless.cpp index f910aab7b3..1db8eb2b1e 100644 --- a/src/thinsrv/headless/headless.cpp +++ b/src/thinsrv/headless/headless.cpp @@ -84,11 +84,6 @@ bool start() { QCommandLineOption recordOption("record", "Record sessions", "path"); parser.addOption(recordOption); -#ifndef NDEBUG - QCommandLineOption lagOption("random-lag", "Randomly sleep to simulate lag", "msecs", "0"); - parser.addOption(lagOption); -#endif - #ifdef HAVE_WEBADMIN // --web-admin-port QCommandLineOption webadminPortOption("web-admin-port", "Web admin interface port", "port", "0"); @@ -253,13 +248,6 @@ bool start() { server->setTemplateDirectory(dir); } -#ifndef NDEBUG - { - uint lag = parser.value(lagOption).toUInt(); - server->setRandomLag(lag); - } -#endif - #ifdef HAVE_WEBADMIN server::Webadmin *webadmin = new server::Webadmin; int webadminPort = parser.value(webadminPortOption).toInt(); diff --git a/src/thinsrv/multiserver.cpp b/src/thinsrv/multiserver.cpp index ed1dbd0275..2c7bfd34bd 100644 --- a/src/thinsrv/multiserver.cpp +++ b/src/thinsrv/multiserver.cpp @@ -49,13 +49,6 @@ MultiServer::MultiServer(ServerConfig *config, QObject *parent) }); } -#ifndef NDEBUG -void MultiServer::setRandomLag(uint lag) -{ - m_sessions->setRandomLag(lag); -} -#endif - /** * @brief Automatically stop server when last session is closed * diff --git a/src/thinsrv/multiserver.h b/src/thinsrv/multiserver.h index 439f913db6..c908c28b2c 100644 --- a/src/thinsrv/multiserver.h +++ b/src/thinsrv/multiserver.h @@ -33,10 +33,6 @@ Q_OBJECT void setSessionDirectory(const QDir &dir); void setTemplateDirectory(const QDir &dir); -#ifndef NDEBUG - void setRandomLag(uint lag); -#endif - /** * @brief Get the port the server is running from * @return port number or zero if server is not running diff --git a/src/thinsrv/templatefiles.cpp b/src/thinsrv/templatefiles.cpp index 9f29a287a5..ba24bf597f 100644 --- a/src/thinsrv/templatefiles.cpp +++ b/src/thinsrv/templatefiles.cpp @@ -1,46 +1,54 @@ // SPDX-License-Identifier: GPL-3.0-or-later - -#include "thinsrv/templatefiles.h" -#include "libshared/record/reader.h" +extern "C" { +#include "dpcommon/input.h" +#include "dpengine/player.h" +#include "parson.h" +} #include "libserver/sessionhistory.h" +#include "libshared/net/protover.h" #include "libshared/util/validators.h" - -#include +#include "thinsrv/templatefiles.h" #include +#include namespace server { TemplateFiles::TemplateFiles(const QDir &dir, QObject *parent) - : QObject(parent), m_dir(dir) + : QObject(parent) + , m_dir(dir) { - m_dir.setNameFilters(QStringList() << "*.dprec" << "*.dptxt" << "*.dprecz" << "*.dptxtz" << "*.dprec.*" << "*.dptxt.*"); - m_watcher = new QFileSystemWatcher(QStringList() << dir.absolutePath(), this); - connect(m_watcher, &QFileSystemWatcher::directoryChanged, this, &TemplateFiles::scanDirectory); + m_dir.setNameFilters({"*.dprec", "*.dptxt", "*.dprec.*", "*.dptxt.*"}); + m_watcher = + new QFileSystemWatcher(QStringList() << dir.absolutePath(), this); + connect( + m_watcher, &QFileSystemWatcher::directoryChanged, this, + &TemplateFiles::scanDirectory); scanDirectory(); } void TemplateFiles::scanDirectory() { - qDebug("%s: scanning template directory...", qPrintable(m_dir.absolutePath())); + qDebug( + "%s: scanning template directory...", qPrintable(m_dir.absolutePath())); QHash templates; for(const QFileInfo &f : m_dir.entryInfoList()) { const QString alias = f.baseName(); if(validateSessionIdAlias(alias)) { - if(m_templates.contains(alias) && m_templates[alias].lastmod == f.lastModified()) { + if(m_templates.contains(alias) && + m_templates[alias].lastmod == f.lastModified()) { // Template is unchanged templates[alias] = m_templates[alias]; } else { // Template has been modified or added - templates[alias] = Template { + templates[alias] = Template{ templateFileDescription(f.absoluteFilePath(), alias), - f.absoluteFilePath(), - f.lastModified() - }; - qDebug("%s: template updated", qPrintable(f.absoluteFilePath())); + f.absoluteFilePath(), f.lastModified()}; + qDebug( + "%s: template updated", qPrintable(f.absoluteFilePath())); } } } @@ -67,30 +75,97 @@ QJsonObject TemplateFiles::templateDescription(const QString &alias) const return QJsonObject(); } -QJsonObject TemplateFiles::templateFileDescription(const QString &path, const QString &alias) const +static QString getHeaderString(JSON_Object *header, const char *key) { - recording::Reader reader(path); - recording::Compatibility compat = reader.open(); - if(compat != recording::COMPATIBLE) { - qWarning("%s: template not compatible", qPrintable(path)); - return QJsonObject(); + JSON_Value *value = json_object_get_value(header, key); + if(value && json_value_get_type(value) == JSONString) { + return QString::fromUtf8(json_value_get_string(value)); + } + return QString{}; +} + +static int getHeaderInt(JSON_Object *header, const char *key) +{ + JSON_Value *value = json_object_get_value(header, key); + if(value) { + switch(json_value_get_type(value)) { + case JSONString: + return QString::fromUtf8(json_value_get_string(value)).toInt(); + case JSONNumber: + return int(json_value_get_number(value)); + default: + break; + } + } + return 0; +} + +static bool getHeaderBool(JSON_Object *header, const char *key) +{ + JSON_Value *value = json_object_get_value(header, key); + if(value) { + switch(json_value_get_type(value)) { + case JSONString: { + QString s = QString::fromUtf8(json_value_get_string(value)); + return !s.isEmpty() && + QString::compare(s, "false", Qt::CaseInsensitive) != 0; + } + case JSONNumber: + return json_value_get_number(value) != 0.0; + case JSONBoolean: + return json_value_get_boolean(value); + default: + break; + } } + return false; +} + +static int getHeaderMaxUserCount(JSON_Object *header) +{ + int maxUserCount = getHeaderInt(header, "maxUserCount"); + return maxUserCount <= 0 ? 25 : qBound(1, maxUserCount, 255); +} - QJsonObject desc; - desc["alias"] = alias; - desc["protocol"] = reader.metadata().value("version"); - desc["maxUserCount"] = reader.metadata().value("maxUserCount").toInt(25); - desc["founder"] = reader.metadata().value("founder").toString("-"); - desc["hasPassword"] = !reader.metadata().value("password").toString().isEmpty(); - desc["title"] = reader.metadata().value("title"); - desc["nsfm"] = reader.metadata().value("nsfm").toBool(false); +QJsonObject TemplateFiles::templateFileDescription( + const QString &path, const QString &alias) const +{ + DP_Input *input = DP_file_input_new_from_path(qUtf8Printable(path)); + DP_Player *player = + input ? DP_player_new(DP_PLAYER_TYPE_GUESS, nullptr, input, nullptr) + : nullptr; + if(!player) { + qWarning( + "Error loading template '%s': %s", qUtf8Printable(path), + DP_error()); + return QJsonObject{}; + } else if(!DP_player_compatible(player)) { + qWarning("Incompatible recording '%s'", qUtf8Printable(path)); + DP_player_free(player); + return QJsonObject{}; + } + JSON_Value *header_value = DP_player_header(player); + JSON_Object *header = json_value_get_object(header_value); + QString founder = getHeaderString(header, "founder"); + QJsonObject desc{ + {"alias", alias}, + {"protocol", getHeaderString(header, "version")}, + {"maxUserCount", getHeaderMaxUserCount(header)}, + {"founder", founder.isEmpty() ? "-" : founder}, + {"hasPassword", !getHeaderString(header, "password").isEmpty()}, + {"title", getHeaderString(header, "title")}, + {"nsfm", getHeaderBool(header, "nsfm")}, + }; + + DP_player_free(player); return desc; } bool TemplateFiles::exists(const QString &alias) const { - return m_templates.contains(alias) && !m_templates[alias].description.isEmpty(); + return m_templates.contains(alias) && + !m_templates[alias].description.isEmpty(); } bool TemplateFiles::init(SessionHistory *session) const @@ -98,53 +173,78 @@ bool TemplateFiles::init(SessionHistory *session) const if(!m_templates.contains(session->idAlias())) return false; - recording::Reader reader(m_templates[session->idAlias()].filename); - if(reader.open() != recording::COMPATIBLE) { - qWarning("%s: template not compatible", qPrintable(m_templates[session->idAlias()].filename)); + QString path = m_templates[session->idAlias()].filename; + DP_Input *input = DP_file_input_new_from_path(qUtf8Printable(path)); + DP_Player *player = + input ? DP_player_new(DP_PLAYER_TYPE_GUESS, nullptr, input, nullptr) + : nullptr; + if(!player) { + qWarning( + "Error loading template '%s': %s", qUtf8Printable(path), + DP_error()); + return false; + } else if(!DP_player_compatible(player)) { + qWarning("Incompatible recording '%s'", qUtf8Printable(path)); + DP_player_free(player); return false; } + JSON_Value *header_value = DP_player_header(player); + JSON_Object *header = json_value_get_object(header_value); + // Set session metadata - Q_ASSERT(protocol::ProtocolVersion::fromString(reader.metadata().value("version").toString()) == session->protocolVersion()); - session->setMaxUsers(reader.metadata().value("maxUserCount").toInt(25)); - session->setPasswordHash(reader.metadata().value("password").toString().toUtf8()); - session->setOpwordHash(reader.metadata().value("opword").toString().toUtf8()); - session->setTitle(reader.metadata().value("title").toString()); - - if(reader.metadata().contains("announce")) { - session->addAnnouncement(reader.metadata()["announce"].toString()); + Q_ASSERT( + protocol::ProtocolVersion::fromString( + getHeaderString(header, "version")) == session->protocolVersion()); + session->setMaxUsers(getHeaderMaxUserCount(header)); + session->setPasswordHash(getHeaderString(header, "password").toUtf8()); + session->setOpwordHash(getHeaderString(header, "opword").toUtf8()); + session->setTitle(getHeaderString(header, "title")); + + QString announce = getHeaderString(header, "announce"); + if(!announce.isEmpty()) { + session->addAnnouncement(announce); } SessionHistory::Flags flags; - if(reader.metadata().value("nsfm").toBool()) + if(getHeaderBool(header, "nsfm")) flags |= SessionHistory::Nsfm; - if(reader.metadata().value("persistent").toBool()) + if(getHeaderBool(header, "persistent")) flags |= SessionHistory::Persistent; - if(reader.metadata().value("preserveChat").toBool()) + if(getHeaderBool(header, "preserveChat")) flags |= SessionHistory::PreserveChat; - if(reader.metadata().value("deputies").toBool()) + if(getHeaderBool(header, "deputies")) flags |= SessionHistory::Deputies; session->setFlags(flags); // Set initial history - bool keepReading=true; + bool keepReading = true; do { - recording::MessageRecord r = reader.readNext(); - switch(r.status) { - case recording::MessageRecord::OK: - session->addMessage(protocol::MessagePtr::fromNullable(r.message)); + DP_Message *msg; + DP_PlayerResult result = DP_player_step(player, &msg); + switch(result) { + case DP_PLAYER_SUCCESS: + session->addMessage(net::Message::noinc(msg)); + break; + case DP_PLAYER_ERROR_PARSE: + qWarning( + "Parse error in template %s: %s", + qUtf8Printable(session->idAlias()), DP_error()); break; - case recording::MessageRecord::INVALID: - qWarning("%s: Invalid message (type %d, len %d) in template!", qPrintable(session->idAlias()), r.invalid_type, r.invalid_len); + case DP_PLAYER_RECORDING_END: + keepReading = false; break; - case recording::MessageRecord::END_OF_RECORDING: + default: + qWarning( + "Error in template %s: %s", qUtf8Printable(session->idAlias()), + DP_error()); keepReading = false; break; } } while(keepReading); + DP_player_free(player); return true; } } - diff --git a/src/thinsrv/templatefiles.h b/src/thinsrv/templatefiles.h index 06e846e6d0..c42be3e84c 100644 --- a/src/thinsrv/templatefiles.h +++ b/src/thinsrv/templatefiles.h @@ -1,15 +1,12 @@ // SPDX-License-Identifier: GPL-3.0-or-later - #ifndef DP_SERVER_TEMPLATEFILES_H #define DP_SERVER_TEMPLATEFILES_H - #include "libserver/templateloader.h" - -#include -#include -#include #include +#include #include +#include +#include class QFileSystemWatcher; @@ -17,7 +14,7 @@ namespace server { class TemplateFiles final : public QObject, public TemplateLoader { public: - explicit TemplateFiles(const QDir &dir, QObject *parent=nullptr); + explicit TemplateFiles(const QDir &dir, QObject *parent = nullptr); QJsonArray templateDescriptions() const override; QJsonObject templateDescription(const QString &alias) const override; @@ -29,7 +26,8 @@ private slots: void scanDirectory(); private: - QJsonObject templateFileDescription(const QString &path, const QString &alias) const; + QJsonObject + templateFileDescription(const QString &path, const QString &alias) const; struct Template { QJsonObject description; @@ -37,7 +35,7 @@ private slots: QDateTime lastmod; }; - QHash m_templates; + QHash m_templates; QFileSystemWatcher *m_watcher; QDir m_dir; }; @@ -45,4 +43,3 @@ private slots: } #endif - diff --git a/src/thinsrv/tests/templates.cpp b/src/thinsrv/tests/templates.cpp index 6a389b2b10..7f7538c06a 100644 --- a/src/thinsrv/tests/templates.cpp +++ b/src/thinsrv/tests/templates.cpp @@ -1,22 +1,18 @@ // SPDX-License-Identifier: GPL-3.0-or-later - -#include "thinsrv/templatefiles.h" #include "libserver/inmemoryhistory.h" #include "libshared/net/protover.h" #include "libshared/util/passwordhash.h" #include "libshared/util/ulid.h" - -#include -#include +#include "thinsrv/templatefiles.h" +#include #include #include - -#include +#include +#include using namespace server; -class TestTemplates final : public QObject -{ +class TestTemplates final : public QObject { Q_OBJECT private slots: void testTemplateLoading() @@ -26,8 +22,10 @@ private slots: QDir dir(tempDir.path()); // Copy some test files to the temp dir - QVERIFY(QFile(":test/test-config.cfg").copy(dir.absoluteFilePath("test.cfg"))); - QVERIFY(QFile(":test/test.dptxt").copy(dir.absoluteFilePath("test.dptxt"))); + QVERIFY(QFile(":test/test-config.cfg") + .copy(dir.absoluteFilePath("test.cfg"))); + QVERIFY( + QFile(":test/test.dptxt").copy(dir.absoluteFilePath("test.dptxt"))); QVERIFY(touch(dir.absoluteFilePath("empty.dptxt"))); // Scan templates @@ -47,11 +45,10 @@ private slots: // Try loading the template InMemoryHistory history( - Ulid::make().toString(), - "test", - protocol::ProtocolVersion::fromString(desc.value("protocol").toString()), - desc.value("founder").toString() - ); + Ulid::make().toString(), "test", + protocol::ProtocolVersion::fromString( + desc.value("protocol").toString()), + desc.value("founder").toString()); QVERIFY(templates.init(&history)); @@ -60,23 +57,24 @@ private slots: QCOMPARE(history.maxUsers(), 1); QCOMPARE(history.title(), QString("Test")); QCOMPARE(int(history.flags()), 0); - QCOMPARE(passwordhash::check("qwerty123", history.passwordHash()), true); + QCOMPARE( + passwordhash::check("qwerty123", history.passwordHash()), true); // History content should now match the test template - protocol::MessageList msgs; + net::MessageList msgs; int last; std::tie(msgs, last) = history.getBatch(-1); QCOMPARE(msgs.size(), 2); - QCOMPARE(msgs.at(0)->type(), protocol::MSG_CANVAS_RESIZE); - QCOMPARE(msgs.at(1)->type(), protocol::MSG_LAYER_CREATE); + QCOMPARE(msgs.at(0).type(), DP_MSG_CANVAS_RESIZE); + QCOMPARE(msgs.at(1).type(), DP_MSG_LAYER_CREATE); } private: bool touch(const QString &path) { - QFile f { path }; + QFile f{path}; if(!f.open(QIODevice::WriteOnly)) return false; f.close(); @@ -87,4 +85,3 @@ private slots: QTEST_MAIN(TestTemplates) #include "templates.moc" -