diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index c541ba2..2c0374d 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -4,7 +4,9 @@ project(squawkCORE) set(CMAKE_AUTOMOC ON) find_package(Qt5Core CONFIG REQUIRED) +find_package(Qt5Gui CONFIG REQUIRED) find_package(Qt5Network CONFIG REQUIRED) +find_package(Qt5Xml CONFIG REQUIRED) set(squawkCORE_SRC squawk.cpp @@ -30,5 +32,7 @@ endif() # Use the Widgets module from Qt 5. target_link_libraries(squawkCORE Qt5::Core) target_link_libraries(squawkCORE Qt5::Network) +target_link_libraries(squawkCORE Qt5::Gui) +target_link_libraries(squawkCORE Qt5::Xml) target_link_libraries(squawkCORE qxmpp) target_link_libraries(squawkCORE lmdb) diff --git a/core/account.cpp b/core/account.cpp index bacbb6d..bc7428c 100644 --- a/core/account.cpp +++ b/core/account.cpp @@ -48,37 +48,39 @@ Account::Account(const QString& p_login, const QString& p_server, const QString& config.setPassword(p_password); config.setAutoAcceptSubscriptions(true); - QObject::connect(&client, SIGNAL(connected()), this, SLOT(onClientConnected())); - QObject::connect(&client, SIGNAL(disconnected()), this, SLOT(onClientDisconnected())); - QObject::connect(&client, SIGNAL(presenceReceived(const QXmppPresence&)), this, SLOT(onPresenceReceived(const QXmppPresence&))); - QObject::connect(&client, SIGNAL(messageReceived(const QXmppMessage&)), this, SLOT(onMessageReceived(const QXmppMessage&))); - QObject::connect(&client, SIGNAL(error(QXmppClient::Error)), this, SLOT(onClientError(QXmppClient::Error))); + QObject::connect(&client, &QXmppClient::connected, this, &Account::onClientConnected); + QObject::connect(&client, &QXmppClient::disconnected, this, &Account::onClientDisconnected); + QObject::connect(&client, &QXmppClient::presenceReceived, this, &Account::onPresenceReceived); + QObject::connect(&client, &QXmppClient::messageReceived, this, &Account::onMessageReceived); + QObject::connect(&client, &QXmppClient::error, this, &Account::onClientError); QXmppRosterManager& rm = client.rosterManager(); - QObject::connect(&rm, SIGNAL(rosterReceived()), this, SLOT(onRosterReceived())); - QObject::connect(&rm, SIGNAL(itemAdded(const QString&)), this, SLOT(onRosterItemAdded(const QString&))); - QObject::connect(&rm, SIGNAL(itemRemoved(const QString&)), this, SLOT(onRosterItemRemoved(const QString&))); - QObject::connect(&rm, SIGNAL(itemChanged(const QString&)), this, SLOT(onRosterItemChanged(const QString&))); - //QObject::connect(&rm, SIGNAL(presenceChanged(const QString&, const QString&)), this, SLOT(onRosterPresenceChanged(const QString&, const QString&))); + QObject::connect(&rm, &QXmppRosterManager::rosterReceived, this, &Account::onRosterReceived); + QObject::connect(&rm, &QXmppRosterManager::itemAdded, this, &Account::onRosterItemAdded); + QObject::connect(&rm, &QXmppRosterManager::itemRemoved, this, &Account::onRosterItemRemoved); + QObject::connect(&rm, &QXmppRosterManager::itemChanged, this, &Account::onRosterItemChanged); + //QObject::connect(&rm, &QXmppRosterManager::presenceChanged, this, &Account::onRosterPresenceChanged); client.addExtension(cm); - QObject::connect(cm, SIGNAL(messageReceived(const QXmppMessage&)), this, SLOT(onCarbonMessageReceived(const QXmppMessage&))); - QObject::connect(cm, SIGNAL(messageSent(const QXmppMessage&)), this, SLOT(onCarbonMessageSent(const QXmppMessage&))); + QObject::connect(cm, &QXmppCarbonManager::messageReceived, this, &Account::onCarbonMessageReceived); + QObject::connect(cm, &QXmppCarbonManager::messageSent, this, &Account::onCarbonMessageSent); client.addExtension(am); - QObject::connect(am, SIGNAL(logMessage(QXmppLogger::MessageType, const QString&)), this, SLOT(onMamLog(QXmppLogger::MessageType, const QString))); - QObject::connect(am, SIGNAL(archivedMessageReceived(const QString&, const QXmppMessage&)), this, SLOT(onMamMessageReceived(const QString&, const QXmppMessage&))); - QObject::connect(am, SIGNAL(resultsRecieved(const QString&, const QXmppResultSetReply&, bool)), - this, SLOT(onMamResultsReceived(const QString&, const QXmppResultSetReply&, bool))); + QObject::connect(am, &QXmppMamManager::logMessage, this, &Account::onMamLog); + QObject::connect(am, &QXmppMamManager::archivedMessageReceived, this, &Account::onMamMessageReceived); + QObject::connect(am, &QXmppMamManager::resultsRecieved, this, &Account::onMamResultsReceived); client.addExtension(mm); - QObject::connect(mm, SIGNAL(roomAdded(QXmppMucRoom*)), this, SLOT(onMucRoomAdded(QXmppMucRoom*))); + QObject::connect(mm, &QXmppMucManager::roomAdded, this, &Account::onMucRoomAdded); client.addExtension(bm); - QObject::connect(bm, SIGNAL(bookmarksReceived(const QXmppBookmarkSet&)), this, SLOT(bookmarksReceived(const QXmppBookmarkSet&))); + QObject::connect(bm, &QXmppBookmarkManager::bookmarksReceived, this, &Account::bookmarksReceived); + + QXmppVCardManager& vm = client.vCardManager(); + QObject::connect(&vm, &QXmppVCardManager::vCardReceived, this, &Account::onVCardReceived); } Account::~Account() @@ -279,6 +281,19 @@ void Core::Account::addedAccount(const QString& jid) {"name", re.name()}, {"state", state} }); + + if (contact->hasAvatar()) { + if (contact->isAvatarAutoGenerated()) { + cData.insert("avatarType", static_cast(Shared::Avatar::valid)); + } else { + cData.insert("avatarType", static_cast(Shared::Avatar::autocreated)); + } + cData.insert("avatarPath", contact->avatarPath()); + } else { + cData.insert("avatarType", static_cast(Shared::Avatar::empty)); + client.vCardManager().requestVCard(jid); + pendingVCardRequests.insert(jid); + } int grCount = 0; for (QSet::const_iterator itr = gr.begin(), end = gr.end(); itr != end; ++itr) { const QString& groupName = *itr; @@ -296,36 +311,32 @@ void Core::Account::addedAccount(const QString& jid) void Core::Account::handleNewRosterItem(Core::RosterItem* contact) { - - QObject::connect(contact, SIGNAL(needHistory(const QString&, const QString&, const QDateTime&)), this, SLOT(onContactNeedHistory(const QString&, const QString&, const QDateTime&))); - QObject::connect(contact, SIGNAL(historyResponse(const std::list&)), this, SLOT(onContactHistoryResponse(const std::list&))); - QObject::connect(contact, SIGNAL(nameChanged(const QString&)), this, SLOT(onContactNameChanged(const QString&))); + QObject::connect(contact, &RosterItem::needHistory, this, &Account::onContactNeedHistory); + QObject::connect(contact, &RosterItem::historyResponse, this, &Account::onContactHistoryResponse); + QObject::connect(contact, &RosterItem::nameChanged, this, &Account::onContactNameChanged); + QObject::connect(contact, &RosterItem::avatarChanged, this, &Account::onContactAvatarChanged); } void Core::Account::handleNewContact(Core::Contact* contact) { handleNewRosterItem(contact); - QObject::connect(contact, SIGNAL(groupAdded(const QString&)), this, SLOT(onContactGroupAdded(const QString&))); - QObject::connect(contact, SIGNAL(groupRemoved(const QString&)), this, SLOT(onContactGroupRemoved(const QString&))); - QObject::connect(contact, SIGNAL(subscriptionStateChanged(Shared::SubscriptionState)), - this, SLOT(onContactSubscriptionStateChanged(Shared::SubscriptionState))); + QObject::connect(contact, &Contact::groupAdded, this, &Account::onContactGroupAdded); + QObject::connect(contact, &Contact::groupRemoved, this, &Account::onContactGroupRemoved); + QObject::connect(contact, &Contact::subscriptionStateChanged, this, &Account::onContactSubscriptionStateChanged); } void Core::Account::handleNewConference(Core::Conference* contact) { handleNewRosterItem(contact); - QObject::connect(contact, SIGNAL(nickChanged(const QString&)), this, SLOT(onMucNickNameChanged(const QString&))); - QObject::connect(contact, SIGNAL(subjectChanged(const QString&)), this, SLOT(onMucSubjectChanged(const QString&))); - QObject::connect(contact, SIGNAL(joinedChanged(bool)), this, SLOT(onMucJoinedChanged(bool))); - QObject::connect(contact, SIGNAL(autoJoinChanged(bool)), this, SLOT(onMucAutoJoinChanged(bool))); - QObject::connect(contact, SIGNAL(addParticipant(const QString&, const QMap&)), - this, SLOT(onMucAddParticipant(const QString&, const QMap&))); - QObject::connect(contact, SIGNAL(changeParticipant(const QString&, const QMap&)), - this, SLOT(onMucChangeParticipant(const QString&, const QMap&))); - QObject::connect(contact, SIGNAL(removeParticipant(const QString&)), this, SLOT(onMucRemoveParticipant(const QString&))); + QObject::connect(contact, &Conference::nickChanged, this, &Account::onMucNickNameChanged); + QObject::connect(contact, &Conference::subjectChanged, this, &Account::onMucSubjectChanged); + QObject::connect(contact, &Conference::joinedChanged, this, &Account::onMucJoinedChanged); + QObject::connect(contact, &Conference::autoJoinChanged, this, &Account::onMucAutoJoinChanged); + QObject::connect(contact, &Conference::addParticipant, this, &Account::onMucAddParticipant); + QObject::connect(contact, &Conference::changeParticipant, this, &Account::onMucChangeParticipant); + QObject::connect(contact, &Conference::removeParticipant, this, &Account::onMucRemoveParticipant); } - void Core::Account::onPresenceReceived(const QXmppPresence& presence) { QString id = presence.from(); @@ -341,11 +352,45 @@ void Core::Account::onPresenceReceived(const QXmppPresence& presence) } else { qDebug() << "Received a presence for another resource of my " << name << " account, skipping"; } + } else { + if (pendingVCardRequests.find(jid) == pendingVCardRequests.end()) { + std::map::const_iterator itr = contacts.find(jid); + if (itr != contacts.end()) { + Contact* cnt = itr->second; + switch (presence.vCardUpdateType()) { + case QXmppPresence::VCardUpdateNone: //this presence has nothing to do with photo + break; + case QXmppPresence::VCardUpdateNotReady: //let's say the photo didn't change here + break; + case QXmppPresence::VCardUpdateNoPhoto: //there is no photo, need to drop if any + if (!cnt->hasAvatar() || (cnt->hasAvatar() && !cnt->isAvatarAutoGenerated())) { + cnt->setAutoGeneratedAvatar(); + } + break; + case QXmppPresence::VCardUpdateValidPhoto: //there is a photo, need to load + if (cnt->hasAvatar()) { + if (cnt->isAvatarAutoGenerated()) { + client.vCardManager().requestVCard(jid); + pendingVCardRequests.insert(jid); + } else { + if (cnt->avatarHash() != presence.photoHash()) { + client.vCardManager().requestVCard(jid); + pendingVCardRequests.insert(jid); + } + } + } else { + client.vCardManager().requestVCard(jid); + pendingVCardRequests.insert(jid); + } + break; + } + } + } } switch (presence.type()) { case QXmppPresence::Error: - qDebug() << "An error reported by presence from " << id; + qDebug() << "An error reported by presence from" << id << presence.error().text(); break; case QXmppPresence::Available:{ QDateTime lastInteraction = presence.lastUserInteraction(); @@ -1211,3 +1256,43 @@ void Core::Account::renameContactRequest(const QString& jid, const QString& newN rm.renameItem(jid, newName); } } + +void Core::Account::onVCardReceived(const QXmppVCardIq& card) +{ + QString jid = card.from(); + pendingVCardRequests.erase(jid); + RosterItem* item = 0; + + std::map::const_iterator contItr = contacts.find(jid); + if (contItr == contacts.end()) { + std::map::const_iterator confItr = conferences.find(jid); + if (confItr == conferences.end()) { + qDebug() << "received vCard" << jid << "doesn't belong to any of known contacts or conferences, skipping"; + return; + } else { + item = confItr->second; + } + } else { + item = contItr->second; + } + + QByteArray ava = card.photo(); + if (ava.size() > 0) { + item->setAvatar(ava); + } else { + item->setAutoGeneratedAvatar(); + } +} + +void Core::Account::onContactAvatarChanged(Shared::Avatar type, const QString& path) +{ + RosterItem* item = static_cast(sender()); + QMap cData({ + {"avatarType", static_cast(type)} + }); + if (type != Shared::Avatar::empty) { + cData.insert("avatarPath", path); + } + + emit changeContact(item->jid, cData); +} diff --git a/core/account.h b/core/account.h index 21f35d3..c1af059 100644 --- a/core/account.h +++ b/core/account.h @@ -30,6 +30,8 @@ #include #include #include +#include +#include #include "../global.h" #include "contact.h" #include "conference.h" @@ -119,6 +121,7 @@ private: std::map queuedContacts; std::set outOfRosterContacts; + std::set pendingVCardRequests; private slots: void onClientConnected(); @@ -157,8 +160,11 @@ private slots: void onContactSubscriptionStateChanged(Shared::SubscriptionState state); void onContactHistoryResponse(const std::list& list); void onContactNeedHistory(const QString& before, const QString& after, const QDateTime& at); + void onContactAvatarChanged(Shared::Avatar, const QString& path); void onMamLog(QXmppLogger::MessageType type, const QString &msg); + + void onVCardReceived(const QXmppVCardIq& card); private: void addedAccount(const QString &bareJid); diff --git a/core/archive.cpp b/core/archive.cpp index 295b04c..5900df2 100644 --- a/core/archive.cpp +++ b/core/archive.cpp @@ -34,6 +34,7 @@ Core::Archive::Archive(const QString& p_jid, QObject* parent): order(), stats(), hasAvatar(false), + avatarAutoGenerated(false), avatarHash(), avatarType() { @@ -82,13 +83,36 @@ void Core::Archive::open(const QString& account) hasAvatar = false; } if (hasAvatar) { - avatarHash = getStatStringValue("avatarHash", txn).c_str(); + try { + avatarAutoGenerated = getStatBoolValue("avatarAutoGenerated", txn); + } catch (NotFound e) { + avatarAutoGenerated = false; + } + avatarType = getStatStringValue("avatarType", txn).c_str(); + if (avatarAutoGenerated) { + avatarHash = ""; + } else { + avatarHash = getStatStringValue("avatarHash", txn).c_str(); + } } else { + avatarAutoGenerated = false; avatarHash = ""; avatarType = ""; } mdb_txn_abort(txn); + + if (hasAvatar) { + QFile ava(path + "/avatar." + avatarType); + if (!ava.exists()) { + bool success = dropAvatar(); + if (!success) { + qDebug() << "error opening archive" << jid << "for account" << account + << ". There is supposed to be avatar but the file doesn't exist, couldn't even drop it, it surely will lead to an error"; + } + } + } + opened = true; } } @@ -577,6 +601,15 @@ bool Core::Archive::getHasAvatar() const return hasAvatar; } +bool Core::Archive::getAutoAvatar() const +{ + if (!opened) { + throw Closed("getAutoAvatar", jid.toStdString()); + } + + return avatarAutoGenerated; +} + QString Core::Archive::getAvatarHash() const { if (!opened) { @@ -594,3 +627,103 @@ QString Core::Archive::getAvatarType() const return avatarType; } + +bool Core::Archive::dropAvatar() +{ + MDB_txn *txn; + mdb_txn_begin(environment, NULL, 0, &txn); + bool success = setStatValue("hasAvatar", false, txn); + success = success && setStatValue("avatarAutoGenerated", false, txn); + success = success && setStatValue("avatarHash", "", txn); + success = success && setStatValue("avatarType", "", txn); + if (!success) { + mdb_txn_abort(txn); + return false; + } else { + hasAvatar = false; + avatarAutoGenerated = false; + avatarHash = ""; + avatarType = ""; + mdb_txn_commit(txn); + return true; + } +} + +bool Core::Archive::setAvatar(const QByteArray& data, bool generated) +{ + if (!opened) { + throw Closed("setAvatar", jid.toStdString()); + } + + if (data.size() == 0) { + if (!hasAvatar) { + return false; + } else { + return dropAvatar(); + } + } else { + const char* cep; + mdb_env_get_path(environment, &cep); + QString currentPath(cep); + bool needToRemoveOld = false; + QCryptographicHash hash(QCryptographicHash::Sha1); + hash.addData(data); + QString newHash(hash.result()); + if (hasAvatar) { + if (!generated && !avatarAutoGenerated && avatarHash == newHash) { + return false; + } + QFile oldAvatar(currentPath + "/avatar." + avatarType); + if (oldAvatar.exists()) { + if (oldAvatar.rename(currentPath + "/avatar." + avatarType + ".bak")) { + needToRemoveOld = true; + } else { + qDebug() << "Can't change avatar: couldn't get rid of the old avatar" << oldAvatar.fileName(); + return false; + } + } + } + QMimeDatabase db; + QMimeType type = db.mimeTypeForData(data); + QString ext = type.preferredSuffix(); + QFile newAvatar(currentPath + "/avatar." + ext); + if (newAvatar.open(QFile::WriteOnly)) { + newAvatar.write(data); + newAvatar.close(); + + MDB_txn *txn; + mdb_txn_begin(environment, NULL, 0, &txn); + bool success = setStatValue("hasAvatar", true, txn); + success = success && setStatValue("avatarAutoGenerated", generated, txn); + success = success && setStatValue("avatarHash", newHash.toStdString(), txn); + success = success && setStatValue("avatarType", ext.toStdString(), txn); + if (!success) { + qDebug() << "Can't change avatar: couldn't store changes to database for" << newAvatar.fileName() << "rolling back to the previous state"; + if (needToRemoveOld) { + QFile oldAvatar(currentPath + "/avatar." + avatarType + ".bak"); + oldAvatar.rename(currentPath + "/avatar." + avatarType); + } + mdb_txn_abort(txn); + return false; + } else { + hasAvatar = true; + avatarAutoGenerated = generated; + avatarHash = newHash; + avatarType = ext; + mdb_txn_commit(txn); + if (needToRemoveOld) { + QFile oldAvatar(currentPath + "/avatar." + avatarType + ".bak"); + oldAvatar.remove(); + } + return true; + } + } else { + qDebug() << "Can't change avatar: cant open file to write" << newAvatar.fileName() << "rolling back to the previous state"; + if (needToRemoveOld) { + QFile oldAvatar(currentPath + "/avatar." + avatarType + ".bak"); + oldAvatar.rename(currentPath + "/avatar." + avatarType); + } + return false; + } + } +} diff --git a/core/archive.h b/core/archive.h index 45a7599..e94fac8 100644 --- a/core/archive.h +++ b/core/archive.h @@ -20,6 +20,10 @@ #define CORE_ARCHIVE_H #include +#include +#include +#include + #include "../global.h" #include #include "../exception.h" @@ -50,8 +54,10 @@ public: bool isFromTheBeginning(); void setFromTheBeginning(bool is); bool getHasAvatar() const; + bool getAutoAvatar() const; QString getAvatarHash() const; QString getAvatarType() const; + bool setAvatar(const QByteArray& data, bool generated = false); public: const QString jid; @@ -135,6 +141,7 @@ private: MDB_dbi order; MDB_dbi stats; bool hasAvatar; + bool avatarAutoGenerated; QString avatarHash; QString avatarType; @@ -145,6 +152,7 @@ private: bool setStatValue(const std::string& id, const std::string& value, MDB_txn* txn); void printOrder(); void printKeys(); + bool dropAvatar(); }; } diff --git a/core/rosteritem.cpp b/core/rosteritem.cpp index 50b3b83..f144901 100644 --- a/core/rosteritem.cpp +++ b/core/rosteritem.cpp @@ -338,9 +338,57 @@ QString Core::RosterItem::avatarHash() const return archive->getAvatarHash(); } +bool Core::RosterItem::isAvatarAutoGenerated() const +{ + return archive->getAutoAvatar(); +} + QString Core::RosterItem::avatarPath() const { QString path(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)); path += "/" + account + "/" + jid + "/avatar." + archive->getAvatarType(); return path; } + +bool Core::RosterItem::hasAvatar() const +{ + return archive->getHasAvatar(); +} + +void Core::RosterItem::setAvatar(const QByteArray& data) +{ + if (archive->setAvatar(data, false)) { + if (archive->getHasAvatar()) { + emit avatarChanged(Shared::Avatar::empty, ""); + } else { + emit avatarChanged(Shared::Avatar::valid, avatarPath()); + } + } +} + +void Core::RosterItem::setAutoGeneratedAvatar() +{ + QImage image(96, 96, QImage::Format_ARGB32_Premultiplied); + QPainter painter(&image); + quint8 colorIndex = rand() % Shared::colorPalette.size(); + const QColor& bg = Shared::colorPalette[colorIndex]; + painter.fillRect(image.rect(), bg); + QFont f; + f.setBold(true); + f.setPixelSize(72); + painter.setFont(f); + if (bg.lightnessF() > 0.5) { + painter.setPen(Qt::black); + } else { + painter.setPen(Qt::white); + } + painter.drawText(image.rect(), Qt::AlignCenter | Qt::AlignVCenter, jid.at(0).toUpper()); + QByteArray arr; + QBuffer stream(&arr); + stream.open(QBuffer::WriteOnly); + image.save(&stream, "PNG"); + stream.close(); + if (archive->setAvatar(arr, true)) { + emit avatarChanged(Shared::Avatar::autocreated, avatarPath()); + } +} diff --git a/core/rosteritem.h b/core/rosteritem.h index a2bc929..c0a490e 100644 --- a/core/rosteritem.h +++ b/core/rosteritem.h @@ -22,6 +22,9 @@ #include #include #include +#include +#include +#include #include @@ -60,14 +63,18 @@ public: void requestHistory(int count, const QString& before); void requestFromEmpty(int count, const QString& before); bool hasAvatar() const; + bool isAvatarAutoGenerated() const; QString avatarHash() const; QString avatarPath() const; + void setAvatar(const QByteArray& data); + void setAutoGeneratedAvatar(); signals: void nameChanged(const QString& name); void subscriptionStateChanged(Shared::SubscriptionState state); void historyResponse(const std::list& messages); void needHistory(const QString& before, const QString& after, const QDateTime& afterTime = QDateTime()); + void avatarChanged(Shared::Avatar, const QString& path); public: const QString jid; diff --git a/global.h b/global.h index 77f89bf..ab84655 100644 --- a/global.h +++ b/global.h @@ -24,6 +24,7 @@ #include #include #include +#include namespace Shared { @@ -69,6 +70,12 @@ enum class Role { moderator }; +enum class Avatar { + empty, + autocreated, + valid +}; + static const Availability availabilityHighest = offline; static const Availability availabilityLowest = online; @@ -102,6 +109,45 @@ static const std::deque affiliationNames = {"Unspecified", "Outcast", " static const std::deque roleNames = {"Unspecified", "Nobody", "Visitor", "Participant", "Moderator"}; QString generateUUID(); +static const std::vector colorPalette = { + QColor(244, 27, 63), + QColor(21, 104, 156), + QColor(38, 156, 98), + QColor(247, 103, 101), + QColor(121, 37, 117), + QColor(242, 202, 33), + QColor(168, 22, 63), + QColor(35, 100, 52), + QColor(52, 161, 152), + QColor(239, 53, 111), + QColor(237, 234, 36), + QColor(153, 148, 194), + QColor(211, 102, 151), + QColor(194, 63, 118), + QColor(249, 149, 51), + QColor(244, 206, 109), + QColor(121, 105, 153), + QColor(244, 199, 30), + QColor(28, 112, 28), + QColor(172, 18, 20), + QColor(25, 66, 110), + QColor(25, 149, 104), + QColor(214, 148, 0), + QColor(203, 47, 57), + QColor(4, 54, 84), + QColor(116, 161, 97), + QColor(50, 68, 52), + QColor(237, 179, 20), + QColor(69, 114, 147), + QColor(242, 212, 31), + QColor(248, 19, 20), + QColor(84, 102, 84), + QColor(25, 53, 122), + QColor(91, 91, 109), + QColor(17, 17, 80), + QColor(54, 54, 94) +}; + class Message { public: enum Type {