diff --git a/.gitmodules b/.gitmodules index bbe5364..c205907 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "external/qxmpp"] path = external/qxmpp url = https://github.com/qxmpp-project/qxmpp.git +[submodule "external/signal-protocol-c"] + path = external/signal-protocol-c + url = https://github.com/signalapp/libsignal-protocol-c.git diff --git a/CHANGELOG.md b/CHANGELOG.md index 06c4ce1..bf90231 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,10 +6,14 @@ - requesting the history of the current chat after reconnection - global availability (in drop down list) gets restored after reconnection - status icon in active chat changes when presence of the pen pal changes +- infinite progress when open the dialogue with something that has no history to show ### Improvements - slightly reduced the traffic on the startup by not requesting history of all MUCs - +- completely rewritten message feed, now it works way faster +- OPTIONAL RUNTIME dependency: "KIO Widgets" that is supposed to allow you to open a file in your default file manager +- show in folder now is supposed to try it's best to show file in folder, even you don't have KIO installed +- once uploaded local files don't get second time uploaded - the remote URL is reused ## Squawk 0.1.5 (Jul 29, 2020) ### Bug fixes diff --git a/CMakeLists.txt b/CMakeLists.txt index 771481f..9ca13cb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,117 +1,119 @@ -cmake_minimum_required(VERSION 3.0) -project(squawk) +cmake_minimum_required(VERSION 3.4) +project(squawk VERSION 0.1.6 LANGUAGES CXX) -set(CMAKE_INCLUDE_CURRENT_DIR ON) -set(CMAKE_CXX_STANDARD 14) +cmake_policy(SET CMP0076 NEW) +cmake_policy(SET CMP0079 NEW) +set(CMAKE_CXX_STANDARD 17) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTOUIC ON) set(CMAKE_AUTORCC ON) include(GNUInstallDirs) -include_directories(.) +set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake") -find_package(Qt5Widgets CONFIG REQUIRED) -find_package(Qt5LinguistTools) +add_executable(squawk) +target_include_directories(squawk PRIVATE ${CMAKE_SOURCE_DIR}) -if(NOT CMAKE_BUILD_TYPE) +option(SYSTEM_QXMPP "Use system qxmpp lib" ON) +option(SYSTEM_SIGNAL "Use system signal-protocol-c lib" ON) +option(WITH_KWALLET "Build KWallet support module" ON) +option(WITH_KIO "Build KIO support module" ON) + +# Dependencies +## Qt +find_package(Qt5 COMPONENTS Widgets DBus Gui Xml Network Core REQUIRED) +target_link_libraries(squawk PRIVATE Qt5::Core Qt5::Widgets Qt5::DBus Qt5::Network Qt5::Gui Qt5::Xml) + +## QXmpp +if (SYSTEM_QXMPP) + find_package(QXmpp CONFIG) + + if (NOT QXmpp_FOUND) + set(SYSTEM_QXMPP OFF) + message("QXmpp package wasn't found, trying to build with bundled QXmpp") + else () + message("Building with system QXmpp") + endif () +endif () + +if (NOT SYSTEM_QXMPP) + target_link_libraries(squawk PRIVATE qxmpp) + add_subdirectory(external/qxmpp) +else () + target_link_libraries(squawk PRIVATE QXmpp::QXmpp) +endif () + +# Signal +if (NOT SYSTEM_SIGNAL) + add_subdirectory(external/signal-protocol-c) + add_dependencies(squawk signal-protocol-c) + target_link_libraries(squawk PRIVATE signal-protocol-c) +else () + find_package(Signal REQUIRED) + target_link_libraries(squawk PRIVATE Signal::Signal) +endif () + +## KIO +if (WITH_KIO) + find_package(KF5KIO CONFIG) + + if (NOT KF5KIO_FOUND) + set(WITH_KIO OFF) + message("KIO package wasn't found, KIO support modules wouldn't be built") + else () + target_compile_definitions(squawk PRIVATE WITH_KIO) + message("Building with support of KIO") + endif () +endif () + +## KWallet +if (WITH_KWALLET) + find_package(KF5Wallet CONFIG) + + if (NOT KF5Wallet_FOUND) + set(WITH_KWALLET OFF) + message("KWallet package wasn't found, KWallet support module wouldn't be built") + else () + target_compile_definitions(squawk PRIVATE WITH_KWALLET) + message("Building with support of KWallet") + endif () +endif () + +## LMDB +find_package(LMDB REQUIRED) +target_link_libraries(squawk PRIVATE lmdb) + +# OpenSSL +find_package(OpenSSL REQUIRED) +target_link_libraries(squawk PRIVATE OpenSSL::Crypto) + +# Misc +target_link_libraries(squawk PRIVATE simpleCrypt) +target_link_libraries(squawk PRIVATE uuid) + +# Build type +if (NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE Debug) -endif() +endif () -set(CMAKE_CXX_FLAGS_DEBUG "-g -Wall -Wextra") -set(CMAKE_CXX_FLAGS_RELEASE "-O3") message("Build type: ${CMAKE_BUILD_TYPE}") +target_compile_options(squawk PRIVATE + "-Wall;-Wextra" + "$<$:-g>" + "$<$:-O3>" + ) -set(squawk_SRC - main.cpp - exception.cpp - signalcatcher.cpp - shared/global.cpp - shared/utils.cpp - shared/message.cpp - shared/vcard.cpp - shared/icons.cpp -) - -set(squawk_HEAD - exception.h - signalcatcher.h - shared.h - shared/enums.h - shared/message.h - shared/global.h - shared/utils.h - shared/vcard.h - shared/icons.h -) - -configure_file(resources/images/logo.svg squawk.svg COPYONLY) -execute_process(COMMAND convert -background none -size 48x48 squawk.svg squawk48.png WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) -execute_process(COMMAND convert -background none -size 64x64 squawk.svg squawk64.png WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) -execute_process(COMMAND convert -background none -size 128x128 squawk.svg squawk128.png WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) -execute_process(COMMAND convert -background none -size 256x256 squawk.svg squawk256.png WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) - -configure_file(packaging/squawk.desktop squawk.desktop COPYONLY) - -set(TS_FILES - translations/squawk.ru.ts -) -qt5_add_translation(QM_FILES ${TS_FILES}) -add_custom_target(translations ALL DEPENDS ${QM_FILES}) - -qt5_add_resources(RCC resources/resources.qrc) - -option(SYSTEM_QXMPP "Use system qxmpp lib" ON) -option(WITH_KWALLET "Build KWallet support module" ON) - -if (SYSTEM_QXMPP) - find_package(QXmpp CONFIG) - - if (NOT QXmpp_FOUND) - set(SYSTEM_QXMPP OFF) - message("QXmpp package wasn't found, trying to build with bundled QXmpp") - else() - message("Building with system QXmpp") - endif() -endif() - -if(NOT SYSTEM_QXMPP) - add_subdirectory(external/qxmpp) -endif() - -if (WITH_KWALLET) - find_package(KF5Wallet CONFIG) - - if (NOT KF5Wallet_FOUND) - set(WITH_KWALLET OFF) - message("KWallet package wasn't found, KWallet support module wouldn't be built") - else() - add_definitions(-DWITH_KWALLET) - message("Building with support of KWallet") - endif() -endif() - -add_executable(squawk ${squawk_SRC} ${squawk_HEAD} ${RCC}) -target_link_libraries(squawk Qt5::Widgets) - -add_subdirectory(ui) add_subdirectory(core) - add_subdirectory(external/simpleCrypt) - -target_link_libraries(squawk squawkUI) -target_link_libraries(squawk squawkCORE) -target_link_libraries(squawk uuid) - -add_dependencies(${CMAKE_PROJECT_NAME} translations) +add_subdirectory(packaging) +add_subdirectory(plugins) +add_subdirectory(resources) +add_subdirectory(shared) +add_subdirectory(translations) +add_subdirectory(ui) +add_subdirectory(qomemo) # Install the executable install(TARGETS squawk DESTINATION ${CMAKE_INSTALL_BINDIR}) -install(FILES ${QM_FILES} DESTINATION ${CMAKE_INSTALL_DATADIR}/squawk/l10n) -install(FILES ${CMAKE_CURRENT_BINARY_DIR}/squawk.svg DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/scalable/apps) -install(FILES ${CMAKE_CURRENT_BINARY_DIR}/squawk48.png DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/48x48/apps RENAME squawk.png) -install(FILES ${CMAKE_CURRENT_BINARY_DIR}/squawk64.png DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/64x64/apps RENAME squawk.png) -install(FILES ${CMAKE_CURRENT_BINARY_DIR}/squawk128.png DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/128x128/apps RENAME squawk.png) -install(FILES ${CMAKE_CURRENT_BINARY_DIR}/squawk256.png DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/256x256/apps RENAME squawk.png) -install(FILES ${CMAKE_CURRENT_BINARY_DIR}/squawk.desktop DESTINATION ${CMAKE_INSTALL_DATADIR}/applications) diff --git a/README.md b/README.md index 30c6473..f2101d6 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,8 @@ - lmdb - CMake 3.0 or higher - qxmpp 1.1.0 or higher -- kwallet (optional) +- KDE Frameworks: kwallet (optional) +- KDE Frameworks: KIO (optional) ### Getting @@ -67,6 +68,7 @@ Here is the list of keys you can pass to configuration phase of `cmake ..`. - `CMAKE_BUILD_TYPE` - `Debug` just builds showing all warnings, `Release` builds with no warnings and applies optimizations (default is `Debug`) - `SYSTEM_QXMPP` - `True` tries to link against `qxmpp` installed in the system, `False` builds bundled `qxmpp` library (default is `True`) - `WITH_KWALLET` - `True` builds the `KWallet` capability module if `KWallet` is installed and if not goes to `False`. `False` disables `KWallet` support (default is `True`) +- `WITH_KIO` - `True` builds the `KIO` capability module if `KIO` is installed and if not goes to `False`. `False` disables `KIO` support (default is `True`) ## License diff --git a/cmake/FindLMDB.cmake b/cmake/FindLMDB.cmake new file mode 100644 index 0000000..79788f1 --- /dev/null +++ b/cmake/FindLMDB.cmake @@ -0,0 +1,47 @@ +#This file is taken from here https://gitlab.ralph.or.at/causal-rt/causal-cpp/, it was GPLv3 license +#Thank you so much, mr. Ralph Alexander Bariz, I hope you don't mind me using your code + +# Try to find LMDB headers and library. +# +# Usage of this module as follows: +# +# find_package(LMDB) +# +# Variables used by this module, they can change the default behaviour and need +# to be set before calling find_package: +# +# LMDB_ROOT_DIR Set this variable to the root installation of +# LMDB if the module has problems finding the +# proper installation path. +# +# Variables defined by this module: +# +# LMDB_FOUND System has LMDB library/headers. +# LMDB_LIBRARIES The LMDB library. +# LMDB_INCLUDE_DIRS The location of LMDB headers. + +find_path(LMDB_ROOT_DIR + NAMES include/lmdb.h + ) + +find_library(LMDB_LIBRARIES + NAMES lmdb + HINTS ${LMDB_ROOT_DIR}/lib + ) + +find_path(LMDB_INCLUDE_DIRS + NAMES lmdb.h + HINTS ${LMDB_ROOT_DIR}/include + ) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(LMDB DEFAULT_MSG + LMDB_LIBRARIES + LMDB_INCLUDE_DIRS + ) + +mark_as_advanced( + LMDB_ROOT_DIR + LMDB_LIBRARIES + LMDB_INCLUDE_DIRS +) diff --git a/cmake/FindSignal.cmake b/cmake/FindSignal.cmake new file mode 100644 index 0000000..752fed7 --- /dev/null +++ b/cmake/FindSignal.cmake @@ -0,0 +1,15 @@ +find_path(Signal_INCLUDE_DIR NAMES signal/signal_protocol.h) +find_library(Signal_LIBRARY signal-protocol-c) +mark_as_advanced(Signal_INCLUDE_DIR Signal_LIBRARY) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Signal REQUIRED_VARS Signal_LIBRARY Signal_INCLUDE_DIR) + +if (Signal_FOUND AND NOT TARGET Signal::Signal) + add_library(Signal::Signal UNKNOWN IMPORTED) + set_target_properties(Signal::Signal PROPERTIES + IMPORTED_LINK_INTERFACE_LANGUAGES "C" + IMPORTED_LOCATION "${Signal_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${Signal_INCLUDE_DIR}" + ) +endif () diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index b74a055..3b160e2 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -1,46 +1,27 @@ -cmake_minimum_required(VERSION 3.0) -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 - account.cpp - archive.cpp - rosteritem.cpp - contact.cpp - conference.cpp - storage.cpp - networkaccess.cpp - adapterFuctions.cpp - handlers/messagehandler.cpp - handlers/rosterhandler.cpp -) +target_sources(squawk PRIVATE + account.cpp + account.h + adapterFuctions.cpp + archive.cpp + archive.h + conference.cpp + conference.h + contact.cpp + contact.h + main.cpp + networkaccess.cpp + networkaccess.h + rosteritem.cpp + rosteritem.h + signalcatcher.cpp + signalcatcher.h + squawk.cpp + squawk.h + storage.cpp + storage.h + urlstorage.cpp + urlstorage.h + ) +add_subdirectory(handlers) add_subdirectory(passwordStorageEngines) - -# Tell CMake to create the helloworld executable -add_library(squawkCORE ${squawkCORE_SRC}) - - -if(SYSTEM_QXMPP) - get_target_property(QXMPP_INTERFACE_INCLUDE_DIRECTORIES QXmpp::QXmpp INTERFACE_INCLUDE_DIRECTORIES) - target_include_directories(squawkCORE PUBLIC ${QXMPP_INTERFACE_INCLUDE_DIRECTORIES}) -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) -target_link_libraries(squawkCORE simpleCrypt) -if (WITH_KWALLET) - target_link_libraries(squawkCORE kwalletPSE) -endif() diff --git a/core/account.cpp b/core/account.cpp index 094fd3c..d38e889 100644 --- a/core/account.cpp +++ b/core/account.cpp @@ -47,7 +47,8 @@ Account::Account(const QString& p_login, const QString& p_server, const QString& network(p_net), passwordType(Shared::AccountPassword::plain), mh(new MessageHandler(this)), - rh(new RosterHandler(this)) + rh(new RosterHandler(this)), + omemo(new QXmpp::Omemo::Manager()) { config.setUser(p_login); config.setDomain(p_server); @@ -84,12 +85,14 @@ Account::Account(const QString& p_login, const QString& p_server, const QString& QObject::connect(dm, &QXmppDiscoveryManager::itemsReceived, this, &Account::onDiscoveryItemsReceived); QObject::connect(dm, &QXmppDiscoveryManager::infoReceived, this, &Account::onDiscoveryInfoReceived); - QObject::connect(network, &NetworkAccess::uploadFileComplete, mh, &MessageHandler::onFileUploaded); - QObject::connect(network, &NetworkAccess::uploadFileError, mh, &MessageHandler::onFileUploadError); + QObject::connect(network, &NetworkAccess::uploadFileComplete, mh, &MessageHandler::onUploadFileComplete); + QObject::connect(network, &NetworkAccess::downloadFileComplete, mh, &MessageHandler::onDownloadFileComplete); + QObject::connect(network, &NetworkAccess::loadFileError, mh, &MessageHandler::onLoadFileError); client.addExtension(rcpm); QObject::connect(rcpm, &QXmppMessageReceiptManager::messageDelivered, mh, &MessageHandler::onReceiptReceived); - + + client.addExtension(omemo.get()); QString path(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)); path += "/" + name; @@ -155,8 +158,9 @@ Account::~Account() reconnectTimer->stop(); } - QObject::disconnect(network, &NetworkAccess::uploadFileComplete, mh, &MessageHandler::onFileUploaded); - QObject::disconnect(network, &NetworkAccess::uploadFileError, mh, &MessageHandler::onFileUploadError); + QObject::disconnect(network, &NetworkAccess::uploadFileComplete, mh, &MessageHandler::onUploadFileComplete); + QObject::disconnect(network, &NetworkAccess::downloadFileComplete, mh, &MessageHandler::onDownloadFileComplete); + QObject::disconnect(network, &NetworkAccess::loadFileError, mh, &MessageHandler::onLoadFileError); delete mh; delete rh; @@ -402,9 +406,6 @@ QString Core::Account::getFullJid() const { void Core::Account::sendMessage(const Shared::Message& data) { mh->sendMessage(data);} -void Core::Account::sendMessage(const Shared::Message& data, const QString& path) { - mh->sendMessage(data, path);} - void Core::Account::onMamMessageReceived(const QString& queryId, const QXmppMessage& msg) { if (msg.id().size() > 0 && (msg.body().size() > 0 || msg.outOfBandUrl().size() > 0)) { @@ -434,13 +435,13 @@ void Core::Account::requestArchive(const QString& jid, int count, const QString& if (contact == 0) { qDebug() << "An attempt to request archive for" << jid << "in account" << name << ", but the contact with such id wasn't found, skipping"; - emit responseArchive(jid, std::list()); + emit responseArchive(jid, std::list(), true); return; } if (state != Shared::ConnectionState::connected) { qDebug() << "An attempt to request archive for" << jid << "in account" << name << ", but the account is not online, skipping"; - emit responseArchive(contact->jid, std::list()); + emit responseArchive(contact->jid, std::list(), false); } contact->requestHistory(count, before); @@ -552,9 +553,11 @@ void Core::Account::onClientError(QXmppClient::Error err) case QXmppStanza::Error::NotAuthorized: errorText = "Authentication error"; break; +#if (QXMPP_VERSION) < QT_VERSION_CHECK(1, 3, 0) case QXmppStanza::Error::PaymentRequired: errorText = "Payment is required"; break; +#endif case QXmppStanza::Error::RecipientUnavailable: errorText = "Recipient is unavailable"; break; @@ -909,3 +912,16 @@ void Core::Account::handleDisconnection() ownVCardRequestInProgress = false; } +void Core::Account::onContactHistoryResponse(const std::list& list, bool last) +{ + RosterItem* contact = static_cast(sender()); + + qDebug() << "Collected history for contact " << contact->jid << list.size() << "elements"; + if (last) { + qDebug() << "The response contains the first accounted message"; + } + emit responseArchive(contact->jid, list, last); +} + +void Core::Account::requestChangeMessage(const QString& jid, const QString& messageId, const QMap& data){ + mh->requestChangeMessage(jid, messageId, data);} diff --git a/core/account.h b/core/account.h index 49c7ca9..2a3816e 100644 --- a/core/account.h +++ b/core/account.h @@ -30,23 +30,24 @@ #include #include -#include -#include -#include -#include -#include -#include #include #include +#include +#include +#include +#include +#include +#include +#include #include #include #include -#include +#include -#include "shared.h" -#include "contact.h" #include "conference.h" +#include "contact.h" #include "networkaccess.h" +#include "shared/shared.h" #include "handlers/messagehandler.h" #include "handlers/rosterhandler.h" @@ -88,7 +89,6 @@ public: void setPasswordType(Shared::AccountPassword pt); QString getFullJid() const; void sendMessage(const Shared::Message& data); - void sendMessage(const Shared::Message& data, const QString& path); void requestArchive(const QString& jid, int count, const QString& before); void subscribeToContact(const QString& jid, const QString& reason); void unsubscribeFromContact(const QString& jid, const QString& reason); @@ -97,6 +97,7 @@ public: void addContactToGroupRequest(const QString& jid, const QString& groupName); void removeContactFromGroupRequest(const QString& jid, const QString& groupName); void renameContactRequest(const QString& jid, const QString& newName); + void requestChangeMessage(const QString& jid, const QString& messageId, const QMap& data); void setRoomJoined(const QString& jid, bool joined); void setRoomAutoJoin(const QString& jid, bool joined); @@ -127,14 +128,14 @@ signals: void removePresence(const QString& jid, const QString& name); void message(const Shared::Message& data); void changeMessage(const QString& jid, const QString& id, const QMap& data); - void responseArchive(const QString& jid, const std::list& list); + void responseArchive(const QString& jid, const std::list& list, bool last); void error(const QString& text); void addRoomParticipant(const QString& jid, const QString& nickName, const QMap& data); void changeRoomParticipant(const QString& jid, const QString& nickName, const QMap& data); void removeRoomParticipant(const QString& jid, const QString& nickName); void receivedVCard(const QString& jid, const Shared::VCard& card); void uploadFile(const QFileInfo& file, const QUrl& set, const QUrl& get, QMap headers); - void uploadFileError(const QString& messageId, const QString& error); + void uploadFileError(const QString& jid, const QString& messageId, const QString& error); private: QString name; @@ -165,6 +166,8 @@ private: MessageHandler* mh; RosterHandler* rh; + + QScopedPointer omemo; private slots: void onClientStateChange(QXmppClient::State state); @@ -183,6 +186,7 @@ private slots: void onDiscoveryItemsReceived (const QXmppDiscoveryIq& items); void onDiscoveryInfoReceived (const QXmppDiscoveryIq& info); + void onContactHistoryResponse(const std::list& list, bool last); private: void handleDisconnection(); diff --git a/core/archive.cpp b/core/archive.cpp index a1f8b76..96a8c0d 100644 --- a/core/archive.cpp +++ b/core/archive.cpp @@ -271,6 +271,8 @@ void Core::Archive::changeMessage(const QString& id, const QMap 0; QDateTime oTime = msg.getTime(); bool idChange = msg.change(data); + QDateTime nTime = msg.getTime(); + bool orderChange = oTime != nTime; MDB_val lmdbKey, lmdbData; QByteArray ba; @@ -280,15 +282,21 @@ void Core::Archive::changeMessage(const QString& id, const QMap Core::Archive::getBefore(int count, const QString& id) @@ -603,10 +612,10 @@ void Core::Archive::setFromTheBeginning(bool is) MDB_txn *txn; mdb_txn_begin(environment, NULL, 0, &txn); bool success = setStatValue("beginning", is, txn); - if (success != 0) { - mdb_txn_abort(txn); - } else { + if (success) { mdb_txn_commit(txn); + } else { + mdb_txn_abort(txn); } } } diff --git a/core/archive.h b/core/archive.h index dd7a167..47c62dc 100644 --- a/core/archive.h +++ b/core/archive.h @@ -25,7 +25,7 @@ #include #include "shared/message.h" -#include "exception.h" +#include "shared/exception.h" #include #include diff --git a/core/handlers/CMakeLists.txt b/core/handlers/CMakeLists.txt new file mode 100644 index 0000000..6da2ef3 --- /dev/null +++ b/core/handlers/CMakeLists.txt @@ -0,0 +1,6 @@ +target_sources(squawk PRIVATE + messagehandler.cpp + messagehandler.h + rosterhandler.cpp + rosterhandler.h + ) diff --git a/core/handlers/messagehandler.cpp b/core/handlers/messagehandler.cpp index 0f0e09d..54aff53 100644 --- a/core/handlers/messagehandler.cpp +++ b/core/handlers/messagehandler.cpp @@ -23,7 +23,6 @@ Core::MessageHandler::MessageHandler(Core::Account* account): QObject(), acc(account), pendingStateMessages(), - pendingMessages(), uploadingSlotsQueue() { } @@ -168,14 +167,16 @@ void Core::MessageHandler::initializeMessage(Shared::Message& target, const QXmp id = source.id(); #endif target.setId(id); - if (target.getId().size() == 0) { + QString messageId = target.getId(); + if (messageId.size() == 0) { target.generateRandomId(); //TODO out of desperation, I need at least a random ID + messageId = target.getId(); } target.setFrom(source.from()); target.setTo(source.to()); target.setBody(source.body()); target.setForwarded(forwarded); - target.setOutOfBandUrl(source.outOfBandUrl()); + if (guessing) { if (target.getFromJid() == acc->getLogin() + "@" + acc->getServer()) { outgoing = true; @@ -189,6 +190,12 @@ void Core::MessageHandler::initializeMessage(Shared::Message& target, const QXmp } else { target.setCurrentTime(); } + + QString oob = source.outOfBandUrl(); + if (oob.size() > 0) { + target.setAttachPath(acc->network->addMessageAndCheckForPath(oob, acc->getName(), target.getPenPalJid(), messageId)); + } + target.setOutOfBandUrl(oob); } void Core::MessageHandler::logMessage(const QXmppMessage& msg, const QString& reason) @@ -232,11 +239,23 @@ void Core::MessageHandler::onReceiptReceived(const QString& jid, const QString& } } -void Core::MessageHandler::sendMessage(Shared::Message data) +void Core::MessageHandler::sendMessage(const Shared::Message& data) +{ + if (data.getOutOfBandUrl().size() == 0 && data.getAttachPath().size() > 0) { + prepareUpload(data); + } else { + performSending(data); + } +} + +void Core::MessageHandler::performSending(Shared::Message data, bool newMessage) { QString jid = data.getPenPalJid(); QString id = data.getId(); + QString oob = data.getOutOfBandUrl(); RosterItem* ri = acc->rh->getRosterItem(jid); + bool sent = false; + QMap changes; if (acc->state == Shared::ConnectionState::connected) { QXmppMessage msg(acc->getFullJid(), data.getTo(), data.getBody(), data.getThread()); @@ -245,23 +264,16 @@ void Core::MessageHandler::sendMessage(Shared::Message data) #endif msg.setId(id); msg.setType(static_cast(data.getType())); //it is safe here, my type is compatible - msg.setOutOfBandUrl(data.getOutOfBandUrl()); + msg.setOutOfBandUrl(oob); msg.setReceiptRequested(true); - bool sent = acc->client.sendPacket(msg); + sent = acc->client.sendPacket(msg); if (sent) { data.setState(Shared::Message::State::sent); } else { data.setState(Shared::Message::State::error); - data.setErrorText("Couldn't send message via QXMPP library check out logs"); - } - - if (ri != 0) { - ri->appendMessageToArchive(data); - if (sent) { - pendingStateMessages.insert(std::make_pair(id, jid)); - } + data.setErrorText("Couldn't send message: internal QXMPP library error, probably need to check out the logs"); } } else { @@ -269,41 +281,74 @@ void Core::MessageHandler::sendMessage(Shared::Message data) data.setErrorText("You are is offline or reconnecting"); } - emit acc->changeMessage(jid, id, { - {"state", static_cast(data.getState())}, - {"errorText", data.getErrorText()} - }); + Shared::Message::State mstate = data.getState(); + changes.insert("state", static_cast(mstate)); + if (mstate == Shared::Message::State::error) { + changes.insert("errorText", data.getErrorText()); + } + if (oob.size() > 0) { + changes.insert("outOfBandUrl", oob); + } + if (!newMessage) { + changes.insert("stamp", data.getTime()); + } + + if (ri != 0) { + if (newMessage) { + ri->appendMessageToArchive(data); + } else { + ri->changeMessage(id, changes); + } + if (sent) { + pendingStateMessages.insert(std::make_pair(id, jid)); + } else { + pendingStateMessages.erase(id); + } + } + + emit acc->changeMessage(jid, id, changes); } -void Core::MessageHandler::sendMessage(const Shared::Message& data, const QString& path) +void Core::MessageHandler::prepareUpload(const Shared::Message& data) { if (acc->state == Shared::ConnectionState::connected) { + QString jid = data.getPenPalJid(); + QString id = data.getId(); + RosterItem* ri = acc->rh->getRosterItem(jid); + if (!ri) { + qDebug() << "An attempt to initialize upload in" << acc->name << "for pal" << jid << "but the object for this pal wasn't found, something went terrebly wrong, skipping send"; + return; + } + QString path = data.getAttachPath(); QString url = acc->network->getFileRemoteUrl(path); if (url.size() != 0) { sendMessageWithLocalUploadedFile(data, url); } else { - if (acc->network->isUploading(path, data.getId())) { - pendingMessages.emplace(data.getId(), data); + if (acc->network->checkAndAddToUploading(acc->getName(), jid, id, path)) { + ri->appendMessageToArchive(data); + pendingStateMessages.insert(std::make_pair(id, jid)); } else { if (acc->um->serviceFound()) { QFileInfo file(path); if (file.exists() && file.isReadable()) { - uploadingSlotsQueue.emplace_back(path, data); + ri->appendMessageToArchive(data); + pendingStateMessages.insert(std::make_pair(id, jid)); + uploadingSlotsQueue.emplace_back(path, id); if (uploadingSlotsQueue.size() == 1) { acc->um->requestUploadSlot(file); } } else { - onFileUploadError(data.getId(), "Uploading file no longer exists or your system user has no permission to read it"); + handleUploadError(jid, id, "Uploading file no longer exists or your system user has no permission to read it"); qDebug() << "Requested upload slot in account" << acc->name << "for file" << path << "but the file doesn't exist or is not readable"; } } else { - onFileUploadError(data.getId(), "Your server doesn't support file upload service, or it's prohibited for your account"); + handleUploadError(jid, id, "Your server doesn't support file upload service, or it's prohibited for your account"); qDebug() << "Requested upload slot in account" << acc->name << "for file" << path << "but upload manager didn't discover any upload services"; } } } } else { - onFileUploadError(data.getId(), "Account is offline or reconnecting"); + handleUploadError(data.getPenPalJid(), data.getId(), "Account is offline or reconnecting"); qDebug() << "An attempt to send message with not connected account " << acc->name << ", skipping"; } } @@ -314,12 +359,12 @@ void Core::MessageHandler::onUploadSlotReceived(const QXmppHttpUploadSlotIq& slo if (uploadingSlotsQueue.size() == 0) { qDebug() << "HTTP Upload manager of account" << acc->name << "reports about success requesting upload slot, but none was requested"; } else { - const std::pair& pair = uploadingSlotsQueue.front(); - const QString& mId = pair.second.getId(); - acc->network->uploadFile(mId, pair.first, slot.putUrl(), slot.getUrl(), slot.putHeaders()); - pendingMessages.emplace(mId, pair.second); - uploadingSlotsQueue.pop_front(); + const std::pair& pair = uploadingSlotsQueue.front(); + const QString& mId = pair.second; + QString palJid = pendingStateMessages.at(mId); + acc->network->uploadFile({acc->name, palJid, mId}, pair.first, slot.putUrl(), slot.getUrl(), slot.putHeaders()); + uploadingSlotsQueue.pop_front(); if (uploadingSlotsQueue.size() > 0) { acc->um->requestUploadSlot(uploadingSlotsQueue.front().first); } @@ -328,44 +373,111 @@ void Core::MessageHandler::onUploadSlotReceived(const QXmppHttpUploadSlotIq& slo void Core::MessageHandler::onUploadSlotRequestFailed(const QXmppHttpUploadRequestIq& request) { + QString err(request.error().text()); if (uploadingSlotsQueue.size() == 0) { qDebug() << "HTTP Upload manager of account" << acc->name << "reports about an error requesting upload slot, but none was requested"; - qDebug() << request.error().text(); + qDebug() << err; } else { - const std::pair& pair = uploadingSlotsQueue.front(); - qDebug() << "Error requesting upload slot for file" << pair.first << "in account" << acc->name << ":" << request.error().text(); - emit acc->uploadFileError(pair.second.getId(), "Error requesting slot to upload file: " + request.error().text()); + const std::pair& pair = uploadingSlotsQueue.front(); + qDebug() << "Error requesting upload slot for file" << pair.first << "in account" << acc->name << ":" << err; + handleUploadError(pendingStateMessages.at(pair.second), pair.second, err); + uploadingSlotsQueue.pop_front(); if (uploadingSlotsQueue.size() > 0) { acc->um->requestUploadSlot(uploadingSlotsQueue.front().first); } - uploadingSlotsQueue.pop_front(); } } -void Core::MessageHandler::onFileUploaded(const QString& messageId, const QString& url) +void Core::MessageHandler::onDownloadFileComplete(const std::list& msgs, const QString& path) { - std::map::const_iterator itr = pendingMessages.find(messageId); - if (itr != pendingMessages.end()) { - sendMessageWithLocalUploadedFile(itr->second, url); - pendingMessages.erase(itr); + QMap cData = { + {"attachPath", path} + }; + for (const Shared::MessageInfo& info : msgs) { + if (info.account == acc->getName()) { + RosterItem* cnt = acc->rh->getRosterItem(info.jid); + if (cnt != 0) { + if (cnt->changeMessage(info.messageId, cData)) { + emit acc->changeMessage(info.jid, info.messageId, cData); + } + } + } } } -void Core::MessageHandler::onFileUploadError(const QString& messageId, const QString& errMsg) +void Core::MessageHandler::onLoadFileError(const std::list& msgs, const QString& text, bool up) { - std::map::const_iterator itr = pendingMessages.find(messageId); - if (itr != pendingMessages.end()) { - pendingMessages.erase(itr); + if (up) { + for (const Shared::MessageInfo& info : msgs) { + if (info.account == acc->getName()) { + handleUploadError(info.jid, info.messageId, text); + } + } } } -void Core::MessageHandler::sendMessageWithLocalUploadedFile(Shared::Message msg, const QString& url) +void Core::MessageHandler::handleUploadError(const QString& jid, const QString& messageId, const QString& errorText) +{ + emit acc->uploadFileError(jid, messageId, "Error requesting slot to upload file: " + errorText); + pendingStateMessages.erase(jid); + requestChangeMessage(jid, messageId, { + {"state", static_cast(Shared::Message::State::error)}, + {"errorText", errorText} + }); +} + +void Core::MessageHandler::onUploadFileComplete(const std::list& msgs, const QString& path) +{ + for (const Shared::MessageInfo& info : msgs) { + if (info.account == acc->getName()) { + RosterItem* ri = acc->rh->getRosterItem(info.jid); + if (ri != 0) { + Shared::Message msg = ri->getMessage(info.messageId); + sendMessageWithLocalUploadedFile(msg, path, false); + } else { + qDebug() << "A signal received about complete upload to" << acc->name << "for pal" << info.jid << "but the object for this pal wasn't found, something went terrebly wrong, skipping send"; + } + } + } +} + +void Core::MessageHandler::sendMessageWithLocalUploadedFile(Shared::Message msg, const QString& url, bool newMessage) { msg.setOutOfBandUrl(url); - if (msg.getBody().size() == 0) { - msg.setBody(url); - } - sendMessage(msg); + if (msg.getBody().size() == 0) { //not sure why, but most messages do that + msg.setBody(url); //they duplicate oob in body, some of them wouldn't even show an attachment if you don't do that + } + performSending(msg, newMessage); //TODO removal/progress update } + +static const std::set allowerToChangeKeys({ + "attachPath", + "outOfBandUrl", + "state", + "errorText" +}); + +void Core::MessageHandler::requestChangeMessage(const QString& jid, const QString& messageId, const QMap& data) +{ + RosterItem* cnt = acc->rh->getRosterItem(jid); + if (cnt != 0) { + bool allSupported = true; + QString unsupportedString; + for (QMap::const_iterator itr = data.begin(); itr != data.end(); ++itr) { //I need all this madness + if (allowerToChangeKeys.count(itr.key()) != 1) { //to not allow this method + allSupported = false; //to make a message to look like if it was edited + unsupportedString = itr.key(); //basically I needed to control who exaclty calls this method + break; //because the underlying tech assumes that the change is initiated by user + } //not by system + } + if (allSupported) { + cnt->changeMessage(messageId, data); + emit acc->changeMessage(jid, messageId, data); + } else { + qDebug() << "A request to change message" << messageId << "of conversation" << jid << "with following data" << data; + qDebug() << "only limited set of dataFields are supported yet here, and" << unsupportedString << "isn't one of them, skipping"; + } + } +} diff --git a/core/handlers/messagehandler.h b/core/handlers/messagehandler.h index be1545f..9138245 100644 --- a/core/handlers/messagehandler.h +++ b/core/handlers/messagehandler.h @@ -28,6 +28,7 @@ #include #include +#include namespace Core { @@ -44,8 +45,7 @@ public: MessageHandler(Account* account); public: - void sendMessage(Shared::Message data); - void sendMessage(const Shared::Message& data, const QString& path); + void sendMessage(const Shared::Message& data); void initializeMessage(Shared::Message& target, const QXmppMessage& source, bool outgoing = false, bool forwarded = false, bool guessing = false) const; public slots: @@ -55,20 +55,24 @@ public slots: void onReceiptReceived(const QString& jid, const QString& id); void onUploadSlotReceived(const QXmppHttpUploadSlotIq& slot); void onUploadSlotRequestFailed(const QXmppHttpUploadRequestIq& request); - void onFileUploaded(const QString& messageId, const QString& url); - void onFileUploadError(const QString& messageId, const QString& errMsg); + void onDownloadFileComplete(const std::list& msgs, const QString& path); + void onUploadFileComplete(const std::list& msgs, const QString& path); + void onLoadFileError(const std::list& msgs, const QString& path, bool up); + void requestChangeMessage(const QString& jid, const QString& messageId, const QMap& data); private: bool handleChatMessage(const QXmppMessage& msg, bool outgoing = false, bool forwarded = false, bool guessing = false); bool handleGroupMessage(const QXmppMessage& msg, bool outgoing = false, bool forwarded = false, bool guessing = false); void logMessage(const QXmppMessage& msg, const QString& reason = "Message wasn't handled: "); - void sendMessageWithLocalUploadedFile(Shared::Message msg, const QString& url); + void sendMessageWithLocalUploadedFile(Shared::Message msg, const QString& url, bool newMessage = true); + void performSending(Shared::Message data, bool newMessage = true); + void prepareUpload(const Shared::Message& data); + void handleUploadError(const QString& jid, const QString& messageId, const QString& errorText); private: Account* acc; - std::map pendingStateMessages; - std::map pendingMessages; - std::deque> uploadingSlotsQueue; + std::map pendingStateMessages; //key is message id, value is JID + std::deque> uploadingSlotsQueue; }; } diff --git a/core/handlers/rosterhandler.cpp b/core/handlers/rosterhandler.cpp index 82ca8c3..ce5f1b7 100644 --- a/core/handlers/rosterhandler.cpp +++ b/core/handlers/rosterhandler.cpp @@ -190,7 +190,7 @@ void Core::RosterHandler::removeContactRequest(const QString& jid) void Core::RosterHandler::handleNewRosterItem(Core::RosterItem* contact) { connect(contact, &RosterItem::needHistory, this->acc, &Account::onContactNeedHistory); - connect(contact, &RosterItem::historyResponse, this, &RosterHandler::onContactHistoryResponse); + connect(contact, &RosterItem::historyResponse, this->acc, &Account::onContactHistoryResponse); connect(contact, &RosterItem::nameChanged, this, &RosterHandler::onContactNameChanged); connect(contact, &RosterItem::avatarChanged, this, &RosterHandler::onContactAvatarChanged); connect(contact, &RosterItem::requestVCard, this->acc, &Account::requestVCard); @@ -315,14 +315,6 @@ void Core::RosterHandler::removeFromGroup(const QString& jid, const QString& gro } } -void Core::RosterHandler::onContactHistoryResponse(const std::list& list) -{ - RosterItem* contact = static_cast(sender()); - - qDebug() << "Collected history for contact " << contact->jid << list.size() << "elements"; - emit acc->responseArchive(contact->jid, list); -} - Core::RosterItem * Core::RosterHandler::getRosterItem(const QString& jid) { RosterItem* item = 0; diff --git a/core/handlers/rosterhandler.h b/core/handlers/rosterhandler.h index c01f396..b1dfc45 100644 --- a/core/handlers/rosterhandler.h +++ b/core/handlers/rosterhandler.h @@ -86,7 +86,6 @@ private slots: void onContactGroupRemoved(const QString& group); void onContactNameChanged(const QString& name); void onContactSubscriptionStateChanged(Shared::SubscriptionState state); - void onContactHistoryResponse(const std::list& list); void onContactAvatarChanged(Shared::Avatar, const QString& path); private: diff --git a/main.cpp b/core/main.cpp similarity index 87% rename from main.cpp rename to core/main.cpp index 4c4b3ea..0090424 100644 --- a/main.cpp +++ b/core/main.cpp @@ -16,23 +16,26 @@ * along with this program. If not, see . */ -#include "ui/squawk.h" -#include "core/squawk.h" +#include "../shared/global.h" +#include "../shared/messageinfo.h" +#include "../ui/squawk.h" #include "signalcatcher.h" -#include "shared/global.h" -#include -#include -#include -#include -#include +#include "squawk.h" #include +#include #include +#include +#include +#include +#include int main(int argc, char *argv[]) { qRegisterMetaType("Shared::Message"); + qRegisterMetaType("Shared::MessageInfo"); qRegisterMetaType("Shared::VCard"); qRegisterMetaType>("std::list"); + qRegisterMetaType>("std::list"); qRegisterMetaType>("QSet"); qRegisterMetaType("Shared::ConnectionState"); qRegisterMetaType("Shared::Availability"); @@ -96,10 +99,7 @@ int main(int argc, char *argv[]) QObject::connect(&w, &Squawk::connectAccount, squawk, &Core::Squawk::connectAccount); QObject::connect(&w, &Squawk::disconnectAccount, squawk, &Core::Squawk::disconnectAccount); QObject::connect(&w, &Squawk::changeState, squawk, &Core::Squawk::changeState); - QObject::connect(&w, qOverload(&Squawk::sendMessage), - squawk, qOverload(&Core::Squawk::sendMessage)); - QObject::connect(&w, qOverload(&Squawk::sendMessage), - squawk, qOverload(&Core::Squawk::sendMessage)); + QObject::connect(&w, &Squawk::sendMessage, squawk,&Core::Squawk::sendMessage); QObject::connect(&w, &Squawk::requestArchive, squawk, &Core::Squawk::requestArchive); QObject::connect(&w, &Squawk::subscribeContact, squawk, &Core::Squawk::subscribeContact); QObject::connect(&w, &Squawk::unsubscribeContact, squawk, &Core::Squawk::unsubscribeContact); @@ -109,14 +109,14 @@ int main(int argc, char *argv[]) QObject::connect(&w, &Squawk::setRoomAutoJoin, squawk, &Core::Squawk::setRoomAutoJoin); QObject::connect(&w, &Squawk::removeRoomRequest, squawk, &Core::Squawk::removeRoomRequest); QObject::connect(&w, &Squawk::addRoomRequest, squawk, &Core::Squawk::addRoomRequest); - QObject::connect(&w, &Squawk::fileLocalPathRequest, squawk, &Core::Squawk::fileLocalPathRequest); - QObject::connect(&w, &Squawk::downloadFileRequest, squawk, &Core::Squawk::downloadFileRequest); + QObject::connect(&w, &Squawk::fileDownloadRequest, squawk, &Core::Squawk::fileDownloadRequest); QObject::connect(&w, &Squawk::addContactToGroupRequest, squawk, &Core::Squawk::addContactToGroupRequest); QObject::connect(&w, &Squawk::removeContactFromGroupRequest, squawk, &Core::Squawk::removeContactFromGroupRequest); QObject::connect(&w, &Squawk::renameContactRequest, squawk, &Core::Squawk::renameContactRequest); QObject::connect(&w, &Squawk::requestVCard, squawk, &Core::Squawk::requestVCard); QObject::connect(&w, &Squawk::uploadVCard, squawk, &Core::Squawk::uploadVCard); QObject::connect(&w, &Squawk::responsePassword, squawk, &Core::Squawk::responsePassword); + QObject::connect(&w, &Squawk::localPathInvalid, squawk, &Core::Squawk::onLocalPathInvalid); QObject::connect(squawk, &Core::Squawk::newAccount, &w, &Squawk::newAccount); QObject::connect(squawk, &Core::Squawk::addContact, &w, &Squawk::addContact); @@ -141,11 +141,10 @@ int main(int argc, char *argv[]) QObject::connect(squawk, &Core::Squawk::addRoomParticipant, &w, &Squawk::addRoomParticipant); QObject::connect(squawk, &Core::Squawk::changeRoomParticipant, &w, &Squawk::changeRoomParticipant); QObject::connect(squawk, &Core::Squawk::removeRoomParticipant, &w, &Squawk::removeRoomParticipant); - QObject::connect(squawk, &Core::Squawk::fileLocalPathResponse, &w, &Squawk::fileLocalPathResponse); - QObject::connect(squawk, &Core::Squawk::downloadFileProgress, &w, &Squawk::fileProgress); - QObject::connect(squawk, &Core::Squawk::downloadFileError, &w, &Squawk::fileError); - QObject::connect(squawk, &Core::Squawk::uploadFileProgress, &w, &Squawk::fileProgress); - QObject::connect(squawk, &Core::Squawk::uploadFileError, &w, &Squawk::fileError); + QObject::connect(squawk, &Core::Squawk::fileDownloadComplete, &w, &Squawk::fileDownloadComplete); + QObject::connect(squawk, &Core::Squawk::fileUploadComplete, &w, &Squawk::fileUploadComplete); + QObject::connect(squawk, &Core::Squawk::fileProgress, &w, &Squawk::fileProgress); + QObject::connect(squawk, &Core::Squawk::fileError, &w, &Squawk::fileError); QObject::connect(squawk, &Core::Squawk::responseVCard, &w, &Squawk::responseVCard); QObject::connect(squawk, &Core::Squawk::requestPassword, &w, &Squawk::requestPassword); QObject::connect(squawk, &Core::Squawk::ready, &w, &Squawk::readSettings); diff --git a/core/networkaccess.cpp b/core/networkaccess.cpp index 2d66a70..eece379 100644 --- a/core/networkaccess.cpp +++ b/core/networkaccess.cpp @@ -16,13 +16,17 @@ * along with this program. If not, see . */ + +#include +#include + #include "networkaccess.h" Core::NetworkAccess::NetworkAccess(QObject* parent): QObject(parent), running(false), manager(0), - files("files"), + storage("fileURLStorage"), downloads(), uploads() { @@ -33,60 +37,31 @@ Core::NetworkAccess::~NetworkAccess() stop(); } -void Core::NetworkAccess::fileLocalPathRequest(const QString& messageId, const QString& url) +void Core::NetworkAccess::downladFile(const QString& url) { std::map::iterator itr = downloads.find(url); if (itr != downloads.end()) { - Transfer* dwn = itr->second; - std::set::const_iterator mItr = dwn->messages.find(messageId); - if (mItr == dwn->messages.end()) { - dwn->messages.insert(messageId); - } - emit downloadFileProgress(messageId, dwn->progress); + qDebug() << "NetworkAccess received a request to download a file" << url << ", but the file is currently downloading, skipping"; } else { try { - QString path = files.getRecord(url); - QFileInfo info(path); - if (info.exists() && info.isFile()) { - emit fileLocalPathResponse(messageId, path); + std::pair> p = storage.getPath(url); + if (p.first.size() > 0) { + QFileInfo info(p.first); + if (info.exists() && info.isFile()) { + emit downloadFileComplete(p.second, p.first); + } else { + startDownload(p.second, url); + } } else { - files.removeRecord(url); - emit fileLocalPathResponse(messageId, ""); + startDownload(p.second, url); } } catch (const Archive::NotFound& e) { - emit fileLocalPathResponse(messageId, ""); + qDebug() << "NetworkAccess received a request to download a file" << url << ", but there is now record of which message uses that file, downloading anyway"; + storage.addFile(url); + startDownload(std::list(), url); } catch (const Archive::Unknown& e) { qDebug() << "Error requesting file path:" << e.what(); - emit fileLocalPathResponse(messageId, ""); - } - } -} - -void Core::NetworkAccess::downladFileRequest(const QString& messageId, const QString& url) -{ - std::map::iterator itr = downloads.find(url); - if (itr != downloads.end()) { - Transfer* dwn = itr->second; - std::set::const_iterator mItr = dwn->messages.find(messageId); - if (mItr == dwn->messages.end()) { - dwn->messages.insert(messageId); - } - emit downloadFileProgress(messageId, dwn->progress); - } else { - try { - QString path = files.getRecord(url); - QFileInfo info(path); - if (info.exists() && info.isFile()) { - emit fileLocalPathResponse(messageId, path); - } else { - files.removeRecord(url); - startDownload(messageId, url); - } - } catch (const Archive::NotFound& e) { - startDownload(messageId, url); - } catch (const Archive::Unknown& e) { - qDebug() << "Error requesting file path:" << e.what(); - emit downloadFileError(messageId, QString("Database error: ") + e.what()); + emit loadFileError(std::list(), QString("Database error: ") + e.what(), false); } } } @@ -95,7 +70,7 @@ void Core::NetworkAccess::start() { if (!running) { manager = new QNetworkAccessManager(); - files.open(); + storage.open(); running = true; } } @@ -103,7 +78,7 @@ void Core::NetworkAccess::start() void Core::NetworkAccess::stop() { if (running) { - files.close(); + storage.close(); manager->deleteLater(); manager = 0; running = false; @@ -128,9 +103,7 @@ void Core::NetworkAccess::onDownloadProgress(qint64 bytesReceived, qint64 bytesT qreal total = bytesTotal; qreal progress = received/total; dwn->progress = progress; - for (std::set::const_iterator mItr = dwn->messages.begin(), end = dwn->messages.end(); mItr != end; ++mItr) { - emit downloadFileProgress(*mItr, progress); - } + emit loadFileProgress(dwn->messages, progress, false); } } @@ -146,9 +119,7 @@ void Core::NetworkAccess::onDownloadError(QNetworkReply::NetworkError code) if (errorText.size() > 0) { itr->second->success = false; Transfer* dwn = itr->second; - for (std::set::const_iterator mItr = dwn->messages.begin(), end = dwn->messages.end(); mItr != end; ++mItr) { - emit downloadFileError(*mItr, errorText); - } + emit loadFileError(dwn->messages, errorText, false); } } } @@ -276,61 +247,54 @@ QString Core::NetworkAccess::getErrorText(QNetworkReply::NetworkError code) void Core::NetworkAccess::onDownloadFinished() { - QString path(""); QNetworkReply* rpl = static_cast(sender()); QString url = rpl->url().toString(); std::map::const_iterator itr = downloads.find(url); if (itr == downloads.end()) { - qDebug() << "an error downloading" << url << ": the request is done but seems like noone is waiting for it, skipping"; + qDebug() << "an error downloading" << url << ": the request is done but there is no record of it being downloaded, ignoring"; } else { Transfer* dwn = itr->second; if (dwn->success) { qDebug() << "download success for" << url; QStringList hops = url.split("/"); QString fileName = hops.back(); - QStringList parts = fileName.split("."); - path = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation) + "/"; - QString suffix(""); - QStringList::const_iterator sItr = parts.begin(); - QString realName = *sItr; - ++sItr; - for (QStringList::const_iterator sEnd = parts.end(); sItr != sEnd; ++sItr) { - suffix += "." + (*sItr); + QString jid; + if (dwn->messages.size() > 0) { + jid = dwn->messages.front().jid; } - QString postfix(""); - QFileInfo proposedName(path + realName + postfix + suffix); - int counter = 0; - while (proposedName.exists()) { - postfix = QString("(") + std::to_string(++counter).c_str() + ")"; - proposedName = QFileInfo(path + realName + postfix + suffix); + QString path = prepareDirectory(jid); + if (path.size() > 0) { + path = checkFileName(fileName, path); + + QFile file(path); + if (file.open(QIODevice::WriteOnly)) { + file.write(dwn->reply->readAll()); + file.close(); + storage.setPath(url, path); + qDebug() << "file" << path << "was successfully downloaded"; + } else { + qDebug() << "couldn't save file" << path; + path = QString(); + } } - path = proposedName.absoluteFilePath(); - QFile file(path); - if (file.open(QIODevice::WriteOnly)) { - file.write(dwn->reply->readAll()); - file.close(); - files.addRecord(url, path); - qDebug() << "file" << path << "was successfully downloaded"; + if (path.size() > 0) { + emit downloadFileComplete(dwn->messages, path); } else { - qDebug() << "couldn't save file" << path; - path = ""; + //TODO do I need to handle the failure here or it's already being handled in error? + //emit loadFileError(dwn->messages, path, false); } } - for (std::set::const_iterator mItr = dwn->messages.begin(), end = dwn->messages.end(); mItr != end; ++mItr) { - emit fileLocalPathResponse(*mItr, path); - } - dwn->reply->deleteLater(); delete dwn; downloads.erase(itr); } } -void Core::NetworkAccess::startDownload(const QString& messageId, const QString& url) +void Core::NetworkAccess::startDownload(const std::list& msgs, const QString& url) { - Transfer* dwn = new Transfer({{messageId}, 0, 0, true, "", url, 0}); + Transfer* dwn = new Transfer({msgs, 0, 0, true, "", url, 0}); QNetworkRequest req(url); dwn->reply = manager->get(req); connect(dwn->reply, &QNetworkReply::downloadProgress, this, &NetworkAccess::onDownloadProgress); @@ -341,7 +305,7 @@ void Core::NetworkAccess::startDownload(const QString& messageId, const QString& #endif connect(dwn->reply, &QNetworkReply::finished, this, &NetworkAccess::onDownloadFinished); downloads.insert(std::make_pair(url, dwn)); - emit downloadFileProgress(messageId, 0); + emit loadFileProgress(dwn->messages, 0, false); } void Core::NetworkAccess::onUploadError(QNetworkReply::NetworkError code) @@ -350,16 +314,16 @@ void Core::NetworkAccess::onUploadError(QNetworkReply::NetworkError code) QString url = rpl->url().toString(); std::map::const_iterator itr = uploads.find(url); if (itr == uploads.end()) { - qDebug() << "an error uploading" << url << ": the request is reporting an error but seems like noone is waiting for it, skipping"; + qDebug() << "an error uploading" << url << ": the request is reporting an error but there is no record of it being uploading, ignoring"; } else { QString errorText = getErrorText(code); if (errorText.size() > 0) { itr->second->success = false; Transfer* upl = itr->second; - for (std::set::const_iterator mItr = upl->messages.begin(), end = upl->messages.end(); mItr != end; ++mItr) { - emit uploadFileError(*mItr, errorText); - } + emit loadFileError(upl->messages, errorText, true); } + + //TODO deletion? } } @@ -369,17 +333,14 @@ void Core::NetworkAccess::onUploadFinished() QString url = rpl->url().toString(); std::map::const_iterator itr = uploads.find(url); if (itr == downloads.end()) { - qDebug() << "an error uploading" << url << ": the request is done but seems like no one is waiting for it, skipping"; + qDebug() << "an error uploading" << url << ": the request is done there is no record of it being uploading, ignoring"; } else { Transfer* upl = itr->second; if (upl->success) { qDebug() << "upload success for" << url; - files.addRecord(upl->url, upl->path); - - for (std::set::const_iterator mItr = upl->messages.begin(), end = upl->messages.end(); mItr != end; ++mItr) { - emit fileLocalPathResponse(*mItr, upl->path); - emit uploadFileComplete(*mItr, upl->url); - } + + storage.addFile(upl->messages, upl->url, upl->path); + emit uploadFileComplete(upl->messages, upl->url); } upl->reply->deleteLater(); @@ -403,94 +364,29 @@ void Core::NetworkAccess::onUploadProgress(qint64 bytesReceived, qint64 bytesTot qreal total = bytesTotal; qreal progress = received/total; upl->progress = progress; - for (std::set::const_iterator mItr = upl->messages.begin(), end = upl->messages.end(); mItr != end; ++mItr) { - emit uploadFileProgress(*mItr, progress); - } - } -} - -void Core::NetworkAccess::startUpload(const QString& messageId, const QString& url, const QString& path) -{ - Transfer* upl = new Transfer({{messageId}, 0, 0, true, path, url, 0}); - QNetworkRequest req(url); - QFile* file = new QFile(path); - if (file->open(QIODevice::ReadOnly)) { - upl->reply = manager->put(req, file); - - connect(upl->reply, &QNetworkReply::uploadProgress, this, &NetworkAccess::onUploadProgress); -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) - connect(upl->reply, qOverload(&QNetworkReply::errorOccurred), this, &NetworkAccess::onUploadError); -#else - connect(upl->reply, qOverload(&QNetworkReply::error), this, &NetworkAccess::onUploadError); -#endif - - connect(upl->reply, &QNetworkReply::finished, this, &NetworkAccess::onUploadFinished); - uploads.insert(std::make_pair(url, upl)); - emit downloadFileProgress(messageId, 0); - } else { - qDebug() << "couldn't upload file" << path; - emit uploadFileError(messageId, "Error opening file"); - delete file; - } -} - -void Core::NetworkAccess::uploadFileRequest(const QString& messageId, const QString& url, const QString& path) -{ - std::map::iterator itr = uploads.find(url); - if (itr != uploads.end()) { - Transfer* upl = itr->second; - std::set::const_iterator mItr = upl->messages.find(messageId); - if (mItr == upl->messages.end()) { - upl->messages.insert(messageId); - } - emit uploadFileProgress(messageId, upl->progress); - } else { - try { - QString ePath = files.getRecord(url); - if (ePath == path) { - QFileInfo info(path); - if (info.exists() && info.isFile()) { - emit fileLocalPathResponse(messageId, path); - } else { - files.removeRecord(url); - startUpload(messageId, url, path); - } - } else { - QFileInfo info(path); - if (info.exists() && info.isFile()) { - files.changeRecord(url, path); - emit fileLocalPathResponse(messageId, path); - } else { - files.removeRecord(url); - startUpload(messageId, url, path); - } - } - } catch (const Archive::NotFound& e) { - startUpload(messageId, url, path); - } catch (const Archive::Unknown& e) { - qDebug() << "Error requesting file path on upload:" << e.what(); - emit uploadFileError(messageId, QString("Database error: ") + e.what()); - } + emit loadFileProgress(upl->messages, progress, true); } } QString Core::NetworkAccess::getFileRemoteUrl(const QString& path) { - return ""; //TODO this is a way not to upload some file more then 1 time, here I'm supposed to return that file GET url + QString p; + + try { + p = storage.getUrl(path); + } catch (const Archive::NotFound& err) { + + } catch (...) { + throw; + } + + return p; } -bool Core::NetworkAccess::isUploading(const QString& path, const QString& messageId) -{ - return false; //TODO this is a way to avoid parallel uploading of the same files by different chats - // message is is supposed to be added to the uploading messageids list - // the result should be true if there was an uploading file with this path - // message id can be empty, then it's just to check and not to add -} - -void Core::NetworkAccess::uploadFile(const QString& messageId, const QString& path, const QUrl& put, const QUrl& get, const QMap headers) +void Core::NetworkAccess::uploadFile(const Shared::MessageInfo& info, const QString& path, const QUrl& put, const QUrl& get, const QMap headers) { QFile* file = new QFile(path); - Transfer* upl = new Transfer({{messageId}, 0, 0, true, path, get.toString(), file}); + Transfer* upl = new Transfer({{info}, 0, 0, true, path, get.toString(), file}); QNetworkRequest req(put); for (QMap::const_iterator itr = headers.begin(), end = headers.end(); itr != end; itr++) { req.setRawHeader(itr.key().toUtf8(), itr.value().toUtf8()); @@ -506,10 +402,99 @@ void Core::NetworkAccess::uploadFile(const QString& messageId, const QString& pa #endif connect(upl->reply, &QNetworkReply::finished, this, &NetworkAccess::onUploadFinished); uploads.insert(std::make_pair(put.toString(), upl)); - emit downloadFileProgress(messageId, 0); + emit loadFileProgress(upl->messages, 0, true); } else { qDebug() << "couldn't upload file" << path; - emit uploadFileError(messageId, "Error opening file"); + emit loadFileError(upl->messages, "Error opening file", true); delete file; + delete upl; } } + +void Core::NetworkAccess::registerFile(const QString& url, const QString& account, const QString& jid, const QString& id) +{ + storage.addFile(url, account, jid, id); + std::map::iterator itr = downloads.find(url); + if (itr != downloads.end()) { + itr->second->messages.emplace_back(account, jid, id); //TODO notification is going to happen the next tick, is that okay? + } +} + +void Core::NetworkAccess::registerFile(const QString& url, const QString& path, const QString& account, const QString& jid, const QString& id) +{ + storage.addFile(url, path, account, jid, id); +} + +bool Core::NetworkAccess::checkAndAddToUploading(const QString& acc, const QString& jid, const QString id, const QString path) +{ + for (const std::pair& pair : uploads) { + Transfer* info = pair.second; + if (pair.second->path == path) { + std::list& messages = info->messages; + bool dup = false; + for (const Shared::MessageInfo& info : messages) { + if (info.account == acc && info.jid == jid && info.messageId == id) { + dup = true; + break; + } + } + if (!dup) { + info->messages.emplace_back(acc, jid, id); //TODO notification is going to happen the next tick, is that okay? + return true; + } + } + } + + return false; +} + +QString Core::NetworkAccess::prepareDirectory(const QString& jid) +{ + QString path = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation); + path += "/" + QApplication::applicationName(); + if (jid.size() > 0) { + path += "/" + jid; + } + QDir location(path); + + if (!location.exists()) { + bool res = location.mkpath(path); + if (!res) { + return ""; + } else { + return path; + } + } + return path; +} + +QString Core::NetworkAccess::checkFileName(const QString& name, const QString& path) +{ + QStringList parts = name.split("."); + QString suffix(""); + QStringList::const_iterator sItr = parts.begin(); + QString realName = *sItr; + ++sItr; + for (QStringList::const_iterator sEnd = parts.end(); sItr != sEnd; ++sItr) { + suffix += "." + (*sItr); + } + QString postfix(""); + QFileInfo proposedName(path + "/" + realName + suffix); + int counter = 0; + while (proposedName.exists()) { + QString count = QString("(") + std::to_string(++counter).c_str() + ")"; + proposedName = QFileInfo(path + "/" + realName + count + suffix); + } + + return proposedName.absoluteFilePath(); +} + +QString Core::NetworkAccess::addMessageAndCheckForPath(const QString& url, const QString& account, const QString& jid, const QString& id) +{ + return storage.addMessageAndCheckForPath(url, account, jid, id); +} + +std::list Core::NetworkAccess::reportPathInvalid(const QString& path) +{ + return storage.deletedFile(path); +} diff --git a/core/networkaccess.h b/core/networkaccess.h index 824b1af..5b9eae2 100644 --- a/core/networkaccess.h +++ b/core/networkaccess.h @@ -29,13 +29,15 @@ #include -#include "storage.h" +#include "urlstorage.h" namespace Core { /** * @todo write docs */ + +//TODO Need to describe how to get rid of records when file is no longer reachable; class NetworkAccess : public QObject { Q_OBJECT @@ -48,26 +50,27 @@ public: void stop(); QString getFileRemoteUrl(const QString& path); - bool isUploading(const QString& path, const QString& messageId = ""); - void uploadFile(const QString& messageId, const QString& path, const QUrl& put, const QUrl& get, const QMap headers); + QString addMessageAndCheckForPath(const QString& url, const QString& account, const QString& jid, const QString& id); + void uploadFile(const Shared::MessageInfo& info, const QString& path, const QUrl& put, const QUrl& get, const QMap headers); + bool checkAndAddToUploading(const QString& acc, const QString& jid, const QString id, const QString path); + std::list reportPathInvalid(const QString& path); signals: - void fileLocalPathResponse(const QString& messageId, const QString& path); - void downloadFileProgress(const QString& messageId, qreal value); - void downloadFileError(const QString& messageId, const QString& path); - void uploadFileProgress(const QString& messageId, qreal value); - void uploadFileError(const QString& messageId, const QString& path); - void uploadFileComplete(const QString& messageId, const QString& url); + void loadFileProgress(const std::list& msgs, qreal value, bool up); + void loadFileError(const std::list& msgs, const QString& text, bool up); + void uploadFileComplete(const std::list& msgs, const QString& url); + void downloadFileComplete(const std::list& msgs, const QString& path); public slots: - void fileLocalPathRequest(const QString& messageId, const QString& url); - void downladFileRequest(const QString& messageId, const QString& url); - void uploadFileRequest(const QString& messageId, const QString& url, const QString& path); + void downladFile(const QString& url); + void registerFile(const QString& url, const QString& account, const QString& jid, const QString& id); + void registerFile(const QString& url, const QString& path, const QString& account, const QString& jid, const QString& id); private: - void startDownload(const QString& messageId, const QString& url); - void startUpload(const QString& messageId, const QString& url, const QString& path); + void startDownload(const std::list& msgs, const QString& url); QString getErrorText(QNetworkReply::NetworkError code); + QString prepareDirectory(const QString& jid); + QString checkFileName(const QString& name, const QString& path); private slots: void onDownloadProgress(qint64 bytesReceived, qint64 bytesTotal); @@ -80,12 +83,12 @@ private slots: private: bool running; QNetworkAccessManager* manager; - Storage files; + UrlStorage storage; std::map downloads; std::map uploads; struct Transfer { - std::set messages; + std::list messages; qreal progress; QNetworkReply* reply; bool success; diff --git a/core/passwordStorageEngines/CMakeLists.txt b/core/passwordStorageEngines/CMakeLists.txt index e824f77..4da3873 100644 --- a/core/passwordStorageEngines/CMakeLists.txt +++ b/core/passwordStorageEngines/CMakeLists.txt @@ -1,37 +1,9 @@ -cmake_minimum_required(VERSION 3.0) -project(pse) - -if (WITH_KWALLET) - set(CMAKE_AUTOMOC ON) - - find_package(Qt5Core CONFIG REQUIRED) - find_package(Qt5Gui CONFIG REQUIRED) - - get_target_property(KWALLET_INTERFACE_INCLUDE_DIRECTORIES KF5::Wallet INTERFACE_INCLUDE_DIRECTORIES) - get_target_property(Qt5GUI_INTERFACE_INCLUDE_DIRECTORIES Qt5::Gui INTERFACE_INCLUDE_DIRECTORIES) - - set(kwalletPSE_SRC - kwallet.cpp - ) - - add_library(kwalletPSE ${kwalletPSE_SRC}) - - target_include_directories(kwalletPSE PUBLIC ${KWALLET_INTERFACE_INCLUDE_DIRECTORIES}) - target_include_directories(kwalletPSE PUBLIC ${Qt5GUI_INTERFACE_INCLUDE_DIRECTORIES}) - - target_link_libraries(kwalletPSE Qt5::Core) - - set(kwalletW_SRC - wrappers/kwallet.cpp +if (WITH_KWALLET) + target_sources(squawk PRIVATE + kwallet.cpp + kwallet.h ) - add_library(kwalletWrapper SHARED ${kwalletW_SRC}) - - target_include_directories(kwalletWrapper PUBLIC ${KWALLET_INTERFACE_INCLUDE_DIRECTORIES}) - target_include_directories(kwalletWrapper PUBLIC ${Qt5GUI_INTERFACE_INCLUDE_DIRECTORIES}) - - target_link_libraries(kwalletWrapper KF5::Wallet) - target_link_libraries(kwalletWrapper Qt5::Core) - - install(TARGETS kwalletWrapper DESTINATION ${CMAKE_INSTALL_LIBDIR}) -endif() + add_subdirectory(wrappers) + target_include_directories(squawk PRIVATE $) +endif () diff --git a/core/passwordStorageEngines/wrappers/CMakeLists.txt b/core/passwordStorageEngines/wrappers/CMakeLists.txt new file mode 100644 index 0000000..6d486c0 --- /dev/null +++ b/core/passwordStorageEngines/wrappers/CMakeLists.txt @@ -0,0 +1,2 @@ +add_library(kwalletWrapper SHARED kwallet.cpp) +target_link_libraries(kwalletWrapper PRIVATE KF5::Wallet) diff --git a/core/rosteritem.cpp b/core/rosteritem.cpp index 32b70f4..b1951d6 100644 --- a/core/rosteritem.cpp +++ b/core/rosteritem.cpp @@ -122,7 +122,22 @@ void Core::RosterItem::nextRequest() { if (syncronizing) { if (requestedCount != -1) { - emit historyResponse(responseCache); + bool last = false; + if (archiveState == beginning || archiveState == complete) { + QString firstId = archive->oldestId(); + if (responseCache.size() == 0) { + if (requestedBefore == firstId) { + last = true; + } + } else { + if (responseCache.front().getId() == firstId) { + last = true; + } + } + } else if (archiveState == empty && responseCache.size() == 0) { + last = true; + } + emit historyResponse(responseCache, last); } } if (requestCache.size() > 0) { @@ -360,6 +375,11 @@ void Core::RosterItem::flushMessagesToArchive(bool finished, const QString& firs archiveState = complete; archive->setFromTheBeginning(true); } + if (added == 0 && wasEmpty) { + archiveState = empty; + nextRequest(); + break; + } if (requestedCount != -1) { QString before; if (responseCache.size() > 0) { @@ -378,7 +398,7 @@ void Core::RosterItem::flushMessagesToArchive(bool finished, const QString& firs } catch (const Archive::Empty& e) { } - if (!found || requestedCount > responseCache.size()) { + if (!found || requestedCount > int(responseCache.size())) { if (archiveState == complete) { nextRequest(); } else { @@ -529,7 +549,7 @@ void Core::RosterItem::clearArchiveRequests() requestedBefore = ""; for (const std::pair& pair : requestCache) { if (pair.first != -1) { - emit historyResponse(responseCache); //just to notify those who still waits with whatever happened to be left in caches yet + emit historyResponse(responseCache, false); //just to notify those who still waits with whatever happened to be left in caches yet } responseCache.clear(); } @@ -549,3 +569,20 @@ void Core::RosterItem::downgradeDatabaseState() archiveState = ArchiveState::chunk; } } + +Shared::Message Core::RosterItem::getMessage(const QString& id) +{ + for (const Shared::Message& msg : appendCache) { + if (msg.getId() == id) { + return msg; + } + } + + for (Shared::Message& msg : hisoryCache) { + if (msg.getId() == id) { + return msg; + } + } + + return archive->getElement(id); +} diff --git a/core/rosteritem.h b/core/rosteritem.h index 4113b37..237a46a 100644 --- a/core/rosteritem.h +++ b/core/rosteritem.h @@ -78,10 +78,12 @@ public: void clearArchiveRequests(); void downgradeDatabaseState(); + Shared::Message getMessage(const QString& id); + signals: void nameChanged(const QString& name); void subscriptionStateChanged(Shared::SubscriptionState state); - void historyResponse(const std::list& messages); + void historyResponse(const std::list& messages, bool last); void needHistory(const QString& before, const QString& after, const QDateTime& afterTime = QDateTime()); void avatarChanged(Shared::Avatar, const QString& path); void requestVCard(const QString& jid); diff --git a/signalcatcher.cpp b/core/signalcatcher.cpp similarity index 100% rename from signalcatcher.cpp rename to core/signalcatcher.cpp diff --git a/signalcatcher.h b/core/signalcatcher.h similarity index 100% rename from signalcatcher.h rename to core/signalcatcher.h diff --git a/core/squawk.cpp b/core/squawk.cpp index 1689d71..411d4ab 100644 --- a/core/squawk.cpp +++ b/core/squawk.cpp @@ -32,11 +32,10 @@ Core::Squawk::Squawk(QObject* parent): ,kwallet() #endif { - connect(&network, &NetworkAccess::fileLocalPathResponse, this, &Squawk::fileLocalPathResponse); - connect(&network, &NetworkAccess::downloadFileProgress, this, &Squawk::downloadFileProgress); - connect(&network, &NetworkAccess::downloadFileError, this, &Squawk::downloadFileError); - connect(&network, &NetworkAccess::uploadFileProgress, this, &Squawk::uploadFileProgress); - connect(&network, &NetworkAccess::uploadFileError, this, &Squawk::uploadFileError); + connect(&network, &NetworkAccess::loadFileProgress, this, &Squawk::fileProgress); + connect(&network, &NetworkAccess::loadFileError, this, &Squawk::fileError); + connect(&network, &NetworkAccess::downloadFileComplete, this, &Squawk::fileDownloadComplete); + connect(&network, &NetworkAccess::uploadFileComplete, this, &Squawk::fileUploadComplete); #ifdef WITH_KWALLET if (kwallet.supportState() == PSE::KWallet::success) { @@ -168,7 +167,7 @@ void Core::Squawk::addAccount( connect(acc, &Account::receivedVCard, this, &Squawk::responseVCard); - connect(acc, &Account::uploadFileError, this, &Squawk::uploadFileError); + connect(acc, &Account::uploadFileError, this, &Squawk::onAccountUploadFileError); QMap map = { {"login", login}, @@ -336,17 +335,6 @@ void Core::Squawk::sendMessage(const QString& account, const Shared::Message& da itr->second->sendMessage(data); } -void Core::Squawk::sendMessage(const QString& account, const Shared::Message& data, const QString& path) -{ - AccountsMap::const_iterator itr = amap.find(account); - if (itr == amap.end()) { - qDebug("An attempt to send a message with non existing account, skipping"); - return; - } - - itr->second->sendMessage(data, path); -} - void Core::Squawk::requestArchive(const QString& account, const QString& jid, int count, const QString& before) { AccountsMap::const_iterator itr = amap.find(account); @@ -357,10 +345,10 @@ void Core::Squawk::requestArchive(const QString& account, const QString& jid, in itr->second->requestArchive(jid, count, before); } -void Core::Squawk::onAccountResponseArchive(const QString& jid, const std::list& list) +void Core::Squawk::onAccountResponseArchive(const QString& jid, const std::list& list, bool last) { Account* acc = static_cast(sender()); - emit responseArchive(acc->getName(), jid, list); + emit responseArchive(acc->getName(), jid, list, last); } void Core::Squawk::modifyAccountRequest(const QString& name, const QMap& map) @@ -604,14 +592,9 @@ void Core::Squawk::addRoomRequest(const QString& account, const QString& jid, co itr->second->addRoomRequest(jid, nick, password, autoJoin); } -void Core::Squawk::fileLocalPathRequest(const QString& messageId, const QString& url) +void Core::Squawk::fileDownloadRequest(const QString& url) { - network.fileLocalPathRequest(messageId, url); -} - -void Core::Squawk::downloadFileRequest(const QString& messageId, const QString& url) -{ - network.downladFileRequest(messageId, url); + network.downladFile(url); } void Core::Squawk::addContactToGroupRequest(const QString& account, const QString& jid, const QString& groupName) @@ -688,7 +671,7 @@ void Core::Squawk::readSettings() settings.value("login").toString(), settings.value("server").toString(), settings.value("password", "").toString(), - settings.value("name").toString(), + settings.value("name").toString(), settings.value("resource").toString(), Shared::Global::fromInt(settings.value("passwordType", static_cast(Shared::AccountPassword::plain)).toInt()) ); @@ -762,3 +745,26 @@ void Core::Squawk::onWalletResponsePassword(const QString& login, const QString& emit changeAccount(login, {{"password", password}}); accountReady(); } + +void Core::Squawk::onAccountUploadFileError(const QString& jid, const QString id, const QString& errorText) +{ + Account* acc = static_cast(sender()); + emit fileError({{acc->getName(), jid, id}}, errorText, true); +} + +void Core::Squawk::onLocalPathInvalid(const QString& path) +{ + std::list list = network.reportPathInvalid(path); + + QMap data({ + {"attachPath", ""} + }); + for (const Shared::MessageInfo& info : list) { + AccountsMap::const_iterator itr = amap.find(info.account); + if (itr != amap.end()) { + itr->second->requestChangeMessage(info.jid, info.messageId, data); + } else { + qDebug() << "Reacting on failure to reach file" << path << "there was an attempt to change message in account" << info.account << "which doesn't exist, skipping"; + } + } +} diff --git a/core/squawk.h b/core/squawk.h index 31812d2..25fdbda 100644 --- a/core/squawk.h +++ b/core/squawk.h @@ -51,31 +51,39 @@ public: signals: void quit(); void ready(); + void newAccount(const QMap&); void changeAccount(const QString& account, const QMap& data); void removeAccount(const QString& account); + void addGroup(const QString& account, const QString& name); void removeGroup(const QString& account, const QString& name); + void addContact(const QString& account, const QString& jid, const QString& group, const QMap& data); void removeContact(const QString& account, const QString& jid); void removeContact(const QString& account, const QString& jid, const QString& group); void changeContact(const QString& account, const QString& jid, const QMap& data); + void addPresence(const QString& account, const QString& jid, const QString& name, const QMap& data); void removePresence(const QString& account, const QString& jid, const QString& name); + void stateChanged(Shared::Availability state); + void accountMessage(const QString& account, const Shared::Message& data); - void responseArchive(const QString& account, const QString& jid, const std::list& list); + void responseArchive(const QString& account, const QString& jid, const std::list& list, bool last); + void addRoom(const QString& account, const QString jid, const QMap& data); void changeRoom(const QString& account, const QString jid, const QMap& data); void removeRoom(const QString& account, const QString jid); void addRoomParticipant(const QString& account, const QString& jid, const QString& name, const QMap& data); void changeRoomParticipant(const QString& account, const QString& jid, const QString& name, const QMap& data); void removeRoomParticipant(const QString& account, const QString& jid, const QString& name); - void fileLocalPathResponse(const QString& messageId, const QString& path); - void downloadFileError(const QString& messageId, const QString& error); - void downloadFileProgress(const QString& messageId, qreal value); - void uploadFileError(const QString& messageId, const QString& error); - void uploadFileProgress(const QString& messageId, qreal value); + + void fileError(const std::list msgs, const QString& error, bool up); + void fileProgress(const std::list msgs, qreal value, bool up); + void fileDownloadComplete(const std::list msgs, const QString& path); + void fileUploadComplete(const std::list msgs, const QString& path); + void responseVCard(const QString& jid, const Shared::VCard& card); void changeMessage(const QString& account, const QString& jid, const QString& id, const QMap& data); void requestPassword(const QString& account); @@ -83,15 +91,18 @@ signals: public slots: void start(); void stop(); + void newAccountRequest(const QMap& map); void modifyAccountRequest(const QString& name, const QMap& map); void removeAccountRequest(const QString& name); void connectAccount(const QString& account); void disconnectAccount(const QString& account); + void changeState(Shared::Availability state); + void sendMessage(const QString& account, const Shared::Message& data); - void sendMessage(const QString& account, const Shared::Message& data, const QString& path); void requestArchive(const QString& account, const QString& jid, int count, const QString& before); + void subscribeContact(const QString& account, const QString& jid, const QString& reason); void unsubscribeContact(const QString& account, const QString& jid, const QString& reason); void addContactToGroupRequest(const QString& account, const QString& jid, const QString& groupName); @@ -99,15 +110,18 @@ public slots: void removeContactRequest(const QString& account, const QString& jid); void renameContactRequest(const QString& account, const QString& jid, const QString& newName); void addContactRequest(const QString& account, const QString& jid, const QString& name, const QSet& groups); + void setRoomJoined(const QString& account, const QString& jid, bool joined); void setRoomAutoJoin(const QString& account, const QString& jid, bool joined); void addRoomRequest(const QString& account, const QString& jid, const QString& nick, const QString& password, bool autoJoin); void removeRoomRequest(const QString& account, const QString& jid); - void fileLocalPathRequest(const QString& messageId, const QString& url); - void downloadFileRequest(const QString& messageId, const QString& url); + + void fileDownloadRequest(const QString& url); + void requestVCard(const QString& account, const QString& jid); void uploadVCard(const QString& account, const Shared::VCard& card); void responsePassword(const QString& account, const QString& password); + void onLocalPathInvalid(const QString& path); private: typedef std::deque Accounts; @@ -146,7 +160,7 @@ private slots: void onAccountAddPresence(const QString& jid, const QString& name, const QMap& data); void onAccountRemovePresence(const QString& jid, const QString& name); void onAccountMessage(const Shared::Message& data); - void onAccountResponseArchive(const QString& jid, const std::list& list); + void onAccountResponseArchive(const QString& jid, const std::list& list, bool last); void onAccountAddRoom(const QString jid, const QMap& data); void onAccountChangeRoom(const QString jid, const QMap& data); void onAccountRemoveRoom(const QString jid); @@ -155,6 +169,8 @@ private slots: void onAccountRemoveRoomPresence(const QString& jid, const QString& nick); void onAccountChangeMessage(const QString& jid, const QString& id, const QMap& data); + void onAccountUploadFileError(const QString& jid, const QString id, const QString& errorText); + void onWalletOpened(bool success); void onWalletResponsePassword(const QString& login, const QString& password); void onWalletRejectPassword(const QString& login); diff --git a/core/urlstorage.cpp b/core/urlstorage.cpp new file mode 100644 index 0000000..f59ff62 --- /dev/null +++ b/core/urlstorage.cpp @@ -0,0 +1,491 @@ +/* + * Squawk messenger. + * Copyright (C) 2019 Yury Gubich + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include + +#include "urlstorage.h" + +Core::UrlStorage::UrlStorage(const QString& p_name): + name(p_name), + opened(false), + environment(), + base(), + map() +{ +} + +Core::UrlStorage::~UrlStorage() +{ + close(); +} + +void Core::UrlStorage::open() +{ + if (!opened) { + mdb_env_create(&environment); + QString path(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)); + path += "/" + name; + QDir cache(path); + + if (!cache.exists()) { + bool res = cache.mkpath(path); + if (!res) { + throw Archive::Directory(path.toStdString()); + } + } + + mdb_env_set_maxdbs(environment, 2); + mdb_env_set_mapsize(environment, 10UL * 1024UL * 1024UL); + mdb_env_open(environment, path.toStdString().c_str(), 0, 0664); + + MDB_txn *txn; + mdb_txn_begin(environment, NULL, 0, &txn); + mdb_dbi_open(txn, "base", MDB_CREATE, &base); + mdb_dbi_open(txn, "map", MDB_CREATE, &map); + mdb_txn_commit(txn); + opened = true; + } +} + +void Core::UrlStorage::close() +{ + if (opened) { + mdb_dbi_close(environment, map); + mdb_dbi_close(environment, base); + mdb_env_close(environment); + opened = false; + } +} + +void Core::UrlStorage::writeInfo(const QString& key, const Core::UrlStorage::UrlInfo& info, bool overwrite) +{ + MDB_txn *txn; + mdb_txn_begin(environment, NULL, 0, &txn); + + try { + writeInfo(key, info, txn, overwrite); + mdb_txn_commit(txn); + } catch (...) { + mdb_txn_abort(txn); + throw; + } +} + +void Core::UrlStorage::writeInfo(const QString& key, const Core::UrlStorage::UrlInfo& info, MDB_txn* txn, bool overwrite) +{ + QByteArray ba; + QDataStream ds(&ba, QIODevice::WriteOnly); + info.serialize(ds); + + const std::string& id = key.toStdString(); + MDB_val lmdbKey, lmdbData; + lmdbKey.mv_size = id.size(); + lmdbKey.mv_data = (char*)id.c_str(); + lmdbData.mv_size = ba.size(); + lmdbData.mv_data = (uint8_t*)ba.data(); + + int rc; + rc = mdb_put(txn, base, &lmdbKey, &lmdbData, overwrite ? 0 : MDB_NOOVERWRITE); + + if (rc != 0) { + if (rc == MDB_KEYEXIST) { + if (!overwrite) { + throw Archive::Exist(name.toStdString(), id); + } + } else { + throw Archive::Unknown(name.toStdString(), mdb_strerror(rc)); + } + } + + if (info.hasPath()) { + std::string sp = info.getPath().toStdString(); + lmdbData.mv_size = sp.size(); + lmdbData.mv_data = (char*)sp.c_str(); + rc = mdb_put(txn, map, &lmdbData, &lmdbKey, 0); + if (rc != 0) { + throw Archive::Unknown(name.toStdString(), mdb_strerror(rc)); + } + } +} + +void Core::UrlStorage::readInfo(const QString& key, Core::UrlStorage::UrlInfo& info, MDB_txn* txn) +{ + const std::string& id = key.toStdString(); + MDB_val lmdbKey, lmdbData; + lmdbKey.mv_size = id.size(); + lmdbKey.mv_data = (char*)id.c_str(); + int rc = mdb_get(txn, base, &lmdbKey, &lmdbData); + + if (rc == 0) { + QByteArray ba((char*)lmdbData.mv_data, lmdbData.mv_size); + QDataStream ds(&ba, QIODevice::ReadOnly); + + info.deserialize(ds); + } else if (rc == MDB_NOTFOUND) { + throw Archive::NotFound(id, name.toStdString()); + } else { + throw Archive::Unknown(name.toStdString(), mdb_strerror(rc)); + } +} + +void Core::UrlStorage::readInfo(const QString& key, Core::UrlStorage::UrlInfo& info) +{ + MDB_txn *txn; + mdb_txn_begin(environment, NULL, MDB_RDONLY, &txn); + + try { + readInfo(key, info, txn); + mdb_txn_commit(txn); + } catch (...) { + mdb_txn_abort(txn); + throw; + } +} + +void Core::UrlStorage::addFile(const QString& url) +{ + if (!opened) { + throw Archive::Closed("addFile(no message, no path)", name.toStdString()); + } + + addToInfo(url, "", "", ""); +} + +void Core::UrlStorage::addFile(const QString& url, const QString& path) +{ + if (!opened) { + throw Archive::Closed("addFile(no message, with path)", name.toStdString()); + } + + addToInfo(url, "", "", "", path); +} + +void Core::UrlStorage::addFile(const QString& url, const QString& account, const QString& jid, const QString& id) +{ + if (!opened) { + throw Archive::Closed("addFile(with message, no path)", name.toStdString()); + } + + addToInfo(url, account, jid, id); +} + +void Core::UrlStorage::addFile(const QString& url, const QString& path, const QString& account, const QString& jid, const QString& id) +{ + if (!opened) { + throw Archive::Closed("addFile(with message, with path)", name.toStdString()); + } + + addToInfo(url, account, jid, id, path); +} + +void Core::UrlStorage::addFile(const std::list& msgs, const QString& url, const QString& path) +{ + if (!opened) { + throw Archive::Closed("addFile(with list)", name.toStdString()); + } + + UrlInfo info (path, msgs); + writeInfo(url, info, true);; +} + +QString Core::UrlStorage::addMessageAndCheckForPath(const QString& url, const QString& account, const QString& jid, const QString& id) +{ + if (!opened) { + throw Archive::Closed("addMessageAndCheckForPath", name.toStdString()); + } + + return addToInfo(url, account, jid, id).getPath(); +} + +Core::UrlStorage::UrlInfo Core::UrlStorage::addToInfo(const QString& url, const QString& account, const QString& jid, const QString& id, const QString& path) +{ + UrlInfo info; + MDB_txn *txn; + mdb_txn_begin(environment, NULL, 0, &txn); + + try { + readInfo(url, info, txn); + } catch (const Archive::NotFound& e) { + + } catch (...) { + mdb_txn_abort(txn); + throw; + } + + bool pathChange = false; + bool listChange = false; + if (path != "-s") { + if (info.getPath() != path) { + info.setPath(path); + pathChange = true; + } + } + + if (account.size() > 0 && jid.size() > 0 && id.size() > 0) { + listChange = info.addMessage(account, jid, id); + } + + if (pathChange || listChange) { + try { + writeInfo(url, info, txn, true); + mdb_txn_commit(txn); + } catch (...) { + mdb_txn_abort(txn); + throw; + } + } else { + mdb_txn_abort(txn); + } + + return info; +} + +std::list Core::UrlStorage::setPath(const QString& url, const QString& path) +{ + std::list list; + + MDB_txn *txn; + mdb_txn_begin(environment, NULL, 0, &txn); + UrlInfo info; + + try { + readInfo(url, info, txn); + info.getMessages(list); + } catch (const Archive::NotFound& e) { + } catch (...) { + mdb_txn_abort(txn); + throw; + } + + info.setPath(path); + try { + writeInfo(url, info, txn, true); + mdb_txn_commit(txn); + } catch (...) { + mdb_txn_abort(txn); + throw; + } + + return list; +} + +std::list Core::UrlStorage::removeFile(const QString& url) +{ + std::list list; + + MDB_txn *txn; + mdb_txn_begin(environment, NULL, 0, &txn); + UrlInfo info; + + try { + std::string id = url.toStdString(); + readInfo(url, info, txn); + info.getMessages(list); + + MDB_val lmdbKey; + lmdbKey.mv_size = id.size(); + lmdbKey.mv_data = (char*)id.c_str(); + int rc = mdb_del(txn, base, &lmdbKey, NULL); + if (rc != 0) { + throw Archive::Unknown(name.toStdString(), mdb_strerror(rc)); + } + + if (info.hasPath()) { + std::string path = info.getPath().toStdString(); + lmdbKey.mv_size = path.size(); + lmdbKey.mv_data = (char*)path.c_str(); + + int rc = mdb_del(txn, map, &lmdbKey, NULL); + if (rc != 0) { + throw Archive::Unknown(name.toStdString(), mdb_strerror(rc)); + } + } + mdb_txn_commit(txn); + } catch (...) { + mdb_txn_abort(txn); + throw; + } + + return list; +} + +std::list Core::UrlStorage::deletedFile(const QString& path) +{ + std::list list; + + MDB_txn *txn; + mdb_txn_begin(environment, NULL, 0, &txn); + + try { + std::string spath = path.toStdString(); + + MDB_val lmdbKey, lmdbData; + lmdbKey.mv_size = spath.size(); + lmdbKey.mv_data = (char*)spath.c_str(); + + QString url; + int rc = mdb_get(txn, map, &lmdbKey, &lmdbData); + + if (rc == 0) { + std::string surl((char*)lmdbData.mv_data, lmdbData.mv_size); + url = QString(surl.c_str()); + } else if (rc == MDB_NOTFOUND) { + qDebug() << "Have been asked to remove file" << path << ", which isn't in the database, skipping"; + mdb_txn_abort(txn); + return list; + } else { + throw Archive::Unknown(name.toStdString(), mdb_strerror(rc)); + } + + UrlInfo info; + std::string id = url.toStdString(); + readInfo(url, info, txn); + info.getMessages(list); + info.setPath(QString()); + writeInfo(url, info, txn, true); + + rc = mdb_del(txn, map, &lmdbKey, NULL); + if (rc != 0) { + throw Archive::Unknown(name.toStdString(), mdb_strerror(rc)); + } + + mdb_txn_commit(txn); + } catch (...) { + mdb_txn_abort(txn); + throw; + } + + return list; +} + + +QString Core::UrlStorage::getUrl(const QString& path) +{ + std::list list; + + MDB_txn *txn; + mdb_txn_begin(environment, NULL, MDB_RDONLY, &txn); + + std::string spath = path.toStdString(); + + MDB_val lmdbKey, lmdbData; + lmdbKey.mv_size = spath.size(); + lmdbKey.mv_data = (char*)spath.c_str(); + + QString url; + int rc = mdb_get(txn, map, &lmdbKey, &lmdbData); + + if (rc == 0) { + std::string surl((char*)lmdbData.mv_data, lmdbData.mv_size); + url = QString(surl.c_str()); + + mdb_txn_abort(txn); + return url; + } else if (rc == MDB_NOTFOUND) { + mdb_txn_abort(txn); + throw Archive::NotFound(spath, name.toStdString()); + } else { + mdb_txn_abort(txn); + throw Archive::Unknown(name.toStdString(), mdb_strerror(rc)); + } +} + +std::pair> Core::UrlStorage::getPath(const QString& url) +{ + UrlInfo info; + readInfo(url, info); + std::list container; + info.getMessages(container); + return std::make_pair(info.getPath(), container); +} + +Core::UrlStorage::UrlInfo::UrlInfo(): + localPath(), + messages() {} + +Core::UrlStorage::UrlInfo::UrlInfo(const QString& path): + localPath(path), + messages() {} + +Core::UrlStorage::UrlInfo::UrlInfo(const QString& path, const std::list& msgs): + localPath(path), + messages(msgs) {} + +Core::UrlStorage::UrlInfo::~UrlInfo() {} + +bool Core::UrlStorage::UrlInfo::addMessage(const QString& acc, const QString& jid, const QString& id) +{ + for (const Shared::MessageInfo& info : messages) { + if (info.account == acc && info.jid == jid && info.messageId == id) { + return false; + } + } + messages.emplace_back(acc, jid, id); + return true; +} + +void Core::UrlStorage::UrlInfo::serialize(QDataStream& data) const +{ + data << localPath; + std::list::size_type size = messages.size(); + data << quint32(size); + for (const Shared::MessageInfo& info : messages) { + data << info.account; + data << info.jid; + data << info.messageId; + } +} + +void Core::UrlStorage::UrlInfo::deserialize(QDataStream& data) +{ + data >> localPath; + quint32 size; + data >> size; + for (quint32 i = 0; i < size; ++i) { + messages.emplace_back(); + Shared::MessageInfo& info = messages.back(); + data >> info.account; + data >> info.jid; + data >> info.messageId; + } +} + +void Core::UrlStorage::UrlInfo::getMessages(std::list& container) const +{ + for (const Shared::MessageInfo& info : messages) { + container.emplace_back(info); + } +} + +QString Core::UrlStorage::UrlInfo::getPath() const +{ + return localPath; +} + +bool Core::UrlStorage::UrlInfo::hasPath() const +{ + return localPath.size() > 0; +} + + +void Core::UrlStorage::UrlInfo::setPath(const QString& path) +{ + localPath = path; +} diff --git a/core/urlstorage.h b/core/urlstorage.h new file mode 100644 index 0000000..3dc5c21 --- /dev/null +++ b/core/urlstorage.h @@ -0,0 +1,99 @@ +/* + * Squawk messenger. + * Copyright (C) 2019 Yury Gubich + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef CORE_URLSTORAGE_H +#define CORE_URLSTORAGE_H + +#include +#include +#include +#include + +#include "archive.h" +#include + +namespace Core { + +/** + * @todo write docs + */ +class UrlStorage +{ + class UrlInfo; +public: + UrlStorage(const QString& name); + ~UrlStorage(); + + void open(); + void close(); + + void addFile(const QString& url); + void addFile(const QString& url, const QString& path); + void addFile(const QString& url, const QString& account, const QString& jid, const QString& id); + void addFile(const QString& url, const QString& path, const QString& account, const QString& jid, const QString& id); + void addFile(const std::list& msgs, const QString& url, const QString& path); //this one overwrites all that was + std::list removeFile(const QString& url); //removes entry like it never was in the database, returns affected message infos + std::list deletedFile(const QString& path); //empties the localPath of the entry, returns affected message infos + std::list setPath(const QString& url, const QString& path); + QString getUrl(const QString& path); + QString addMessageAndCheckForPath(const QString& url, const QString& account, const QString& jid, const QString& id); + std::pair> getPath(const QString& url); + +private: + QString name; + bool opened; + MDB_env* environment; + MDB_dbi base; + MDB_dbi map; + +private: + void writeInfo(const QString& key, const UrlInfo& info, bool overwrite = false); + void writeInfo(const QString& key, const UrlInfo& info, MDB_txn* txn, bool overwrite = false); + void readInfo(const QString& key, UrlInfo& info); + void readInfo(const QString& key, UrlInfo& info, MDB_txn* txn); + UrlInfo addToInfo(const QString& url, const QString& account, const QString& jid, const QString& id, const QString& path = "-s"); + +private: + class UrlInfo { + public: + UrlInfo(const QString& path); + UrlInfo(const QString& path, const std::list& msgs); + UrlInfo(); + ~UrlInfo(); + + void serialize(QDataStream& data) const; + void deserialize(QDataStream& data); + + QString getPath() const; + bool hasPath() const; + void setPath(const QString& path); + + bool addMessage(const QString& acc, const QString& jid, const QString& id); + void getMessages(std::list& container) const; + + private: + QString localPath; + std::list messages; + }; + + +}; + +} + +#endif // CORE_URLSTORAGE_H diff --git a/external/signal-protocol-c b/external/signal-protocol-c new file mode 160000 index 0000000..3a83a4f --- /dev/null +++ b/external/signal-protocol-c @@ -0,0 +1 @@ +Subproject commit 3a83a4f4ed2302ff6e68ab569c88793b50c22d28 diff --git a/external/simpleCrypt/CMakeLists.txt b/external/simpleCrypt/CMakeLists.txt index bdb62c6..274d304 100644 --- a/external/simpleCrypt/CMakeLists.txt +++ b/external/simpleCrypt/CMakeLists.txt @@ -1,16 +1,10 @@ cmake_minimum_required(VERSION 3.0) -project(simplecrypt) +project(simplecrypt LANGUAGES CXX) set(CMAKE_AUTOMOC ON) -find_package(Qt5Core CONFIG REQUIRED) +find_package(Qt5 COMPONENTS Core REQUIRED) -set(simplecrypt_SRC - simplecrypt.cpp -) +add_library(simpleCrypt STATIC simplecrypt.cpp simplecrypt.h) -# Tell CMake to create the helloworld executable -add_library(simpleCrypt ${simplecrypt_SRC}) - -# Use the Widgets module from Qt 5. target_link_libraries(simpleCrypt Qt5::Core) diff --git a/packaging/CMakeLists.txt b/packaging/CMakeLists.txt new file mode 100644 index 0000000..4965b37 --- /dev/null +++ b/packaging/CMakeLists.txt @@ -0,0 +1,3 @@ +configure_file(squawk.desktop squawk.desktop COPYONLY) + +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/squawk.desktop DESTINATION ${CMAKE_INSTALL_DATADIR}/applications) \ No newline at end of file diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt new file mode 100644 index 0000000..84fc09b --- /dev/null +++ b/plugins/CMakeLists.txt @@ -0,0 +1,4 @@ +if (WITH_KIO) + add_library(openFileManagerWindowJob SHARED openfilemanagerwindowjob.cpp) + target_link_libraries(openFileManagerWindowJob PRIVATE KF5::KIOWidgets) +endif () diff --git a/plugins/openfilemanagerwindowjob.cpp b/plugins/openfilemanagerwindowjob.cpp new file mode 100644 index 0000000..904fbcf --- /dev/null +++ b/plugins/openfilemanagerwindowjob.cpp @@ -0,0 +1,8 @@ +#include +#include +#include + +extern "C" void highlightInFileManager(const QUrl& url) { + KIO::OpenFileManagerWindowJob* job = KIO::highlightInFileManager({url}); + QObject::connect(job, &KIO::OpenFileManagerWindowJob::result, job, &KIO::OpenFileManagerWindowJob::deleteLater); +} diff --git a/qomemo/CMakeLists.txt b/qomemo/CMakeLists.txt new file mode 100644 index 0000000..7c71af1 --- /dev/null +++ b/qomemo/CMakeLists.txt @@ -0,0 +1,25 @@ +target_sources(squawk PRIVATE + bundle.cpp + bundle.h + database.cpp + database.h + device.cpp + device.h + device_key_storage.cpp + device_key_storage.h + device_service.cpp + device_service.h + key.cpp + key.h + qomemo.cpp + qomemo.h + sce.cpp + sce.h + user_device_list.cpp + user_device_list.h + qxmpp_omemo_manager.cpp + qxmpp_omemo_manager.h + ) + +add_subdirectory(signal) +add_subdirectory(variant) diff --git a/qomemo/bundle.cpp b/qomemo/bundle.cpp new file mode 100644 index 0000000..d27cd1e --- /dev/null +++ b/qomemo/bundle.cpp @@ -0,0 +1,5 @@ +/* + * Created by victoria on 2021-05-12. + */ + +#include "bundle.h" diff --git a/qomemo/bundle.h b/qomemo/bundle.h new file mode 100644 index 0000000..61e8185 --- /dev/null +++ b/qomemo/bundle.h @@ -0,0 +1,34 @@ +/* + * Created by victoria on 2021-05-12. + */ + +#pragma once + +#include +#include +#include + +class QXmppPubSubIq; + +class QXmppElement; + +class QXmppIq; + +namespace QXmpp::Omemo { + + class PreKey { + public: + int id; + QByteArray data; + }; + + class Bundle { + public: + QByteArray spk; + int spkId; + QByteArray spks; + QByteArray ik; + QList prekeys; + }; + +} // namespace QXmpp::Omemo diff --git a/qomemo/database.cpp b/qomemo/database.cpp new file mode 100644 index 0000000..2923634 --- /dev/null +++ b/qomemo/database.cpp @@ -0,0 +1,180 @@ +/* + * Created by victoria on 2021-05-13. + */ + +#include "database.h" + +#include "bundle.h" + +#include +#include +#include +#include +#include + +using namespace QXmpp::Omemo; + +Database::Database(QString jid) : jid(std::move(jid)) { + auto cacheLocation = + QStandardPaths::writableLocation(QStandardPaths::CacheLocation); + auto path = QString("%1/.omemo/%2").arg(cacheLocation, jid); + QDir cache(path); + + if (!cache.exists() && !cache.mkpath(path)) { + qWarning() << "Could not create:" << path; + throw QException(); + } + + mdb_env_create(&env); + + mdb_env_set_maxdbs(env, 5); + mdb_env_set_mapsize(env, 512UL * 1024UL * 1024UL); + mdb_env_open(env, path.toStdString().c_str(), 0, 0664); + + MDB_txn *txn; + mdb_txn_begin(env, nullptr, 0, &txn); + mdb_dbi_open(txn, "keys", MDB_CREATE, &dbiKeys); + mdb_dbi_open(txn, "devices", MDB_CREATE, &dbiDevices); + mdb_dbi_open(txn, "identity_keys", MDB_CREATE, &dbiIdentityKeys); + mdb_txn_commit(txn); +} + +Database::~Database() { + mdb_dbi_close(env, dbiKeys); + mdb_dbi_close(env, dbiDevices); + mdb_dbi_close(env, dbiIdentityKeys); + mdb_env_close(env); +} + +std::optional Database::loadIdentityKeySecret(int deviceId) { return std::nullopt; } + +bool Database::saveIdentityKeySecret(int deviceId, + const QByteArray &identityKeySecret) { + MDB_val mdbKey, mdbValue; + auto key = QString("%1/secret").arg(QString::number(deviceId)).toStdString(); + + mdbKey.mv_data = key.data(); + mdbKey.mv_size = key.size(); + + mdbValue.mv_data = const_cast(identityKeySecret.data()); + mdbValue.mv_size = identityKeySecret.size(); + + MDB_txn *txn; + mdb_txn_begin(env, nullptr, 0, &txn); + auto err = mdb_put(txn, dbiIdentityKeys, &mdbKey, &mdbValue, MDB_NOOVERWRITE); + if (!err) { + mdb_txn_commit(txn); + return true; + } + + qWarning() << "could not save identity key secret:" << mdb_strerror(err); + mdb_txn_abort(txn); + + return false; +} + +std::optional Database::loadActiveDeviceId() { + MDB_val key, value; + + key.mv_data = (void *) "active"; + key.mv_size = sizeof("active"); + + MDB_txn *txn; + mdb_txn_begin(env, nullptr, 0, &txn); + + auto err = mdb_get(txn, dbiIdentityKeys, &key, &value); + if (err) { + qWarning() << "could not load active device id:" << mdb_strerror(err); + mdb_txn_abort(txn); + return std::nullopt; + } + + if (value.mv_size != sizeof(int)) { + qWarning() << "mv_size is" << value.mv_size << "instead of" << sizeof(int); + mdb_txn_abort(txn); + return std::nullopt; + } + + auto id = *reinterpret_cast(value.mv_data); + + mdb_txn_abort(txn); + + return id; +} + +bool Database::saveActiveDeviceId(int deviceId) { + MDB_val key, value; + + key.mv_data = (void *) "active"; + key.mv_size = sizeof("active"); + + value.mv_data = &deviceId; + value.mv_size = sizeof(deviceId); + + MDB_txn *txn; + mdb_txn_begin(env, nullptr, 0, &txn); + + auto err = mdb_put(txn, dbiIdentityKeys, &key, &value, 0); + if (err) { + qWarning() << "could not save active device id" << mdb_strerror(err); + return false; + } + + err = mdb_txn_commit(txn); + if (err) { + qWarning() << "could not save active device id" << mdb_strerror(err); + return false; + } + + return true; +} + +bool Database::saveIdentityKey(int deviceId, const QByteArray &identityKey) { + return false; +} + +std::optional Database::loadIdentityKey(int deviceId) { + return std::nullopt; +} + +bool Database::containsPreKey() { + return false; +} + +std::optional Database::loadPreKey(int deviceId, int id) { + return std::nullopt; +} + +bool Database::savePreKey(int deviceId, int id, const KeyPair &preKey) { + return false; +} + +std::optional Database::loadBundle(int deviceId) { + Bundle result{}; + + auto ik = loadIdentityKey(deviceId); + + result.ik = ik.value(); + + auto spk = loadSignedPreKey(deviceId); + + result.spk = spk->key.publicKey; + result.spks = spk->signature; + result.spkId = spk->id; + + // PreKeys + + return result; +} + +bool Database::saveBundle(int deviceId, const Bundle &bundle) { + return false; +} + +std::optional Database::loadSignedPreKey(int deviceId) { + return std::optional(); +} + +bool Database::saveSignedPreKey(int deviceId, const SignedPreKey &signedPreKey) { + return false; +} diff --git a/qomemo/database.h b/qomemo/database.h new file mode 100644 index 0000000..1edeea6 --- /dev/null +++ b/qomemo/database.h @@ -0,0 +1,56 @@ +/* + * Created by victoria on 2021-05-13. + */ + +#pragma once + +#include +#include +#include + +#include "key.h" + +namespace QXmpp::Omemo { + + class Bundle; + + class Database { + public: + explicit Database(QString jid); + ~Database(); + Database(const Database &) = delete; + Database(Database &&) = delete; + Database &operator=(const Database &) = delete; + + // For local user + std::optional loadActiveDeviceId(); + bool saveActiveDeviceId(int deviceId); + + std::optional loadIdentityKeySecret(int deviceId); + bool saveIdentityKeySecret(int deviceId, const QByteArray &identityKeySecret); + + std::optional loadBundle(int deviceId); + bool saveBundle(int deviceId, const Bundle& bundle); + + // For any user + std::optional loadIdentityKey(int deviceId); + bool saveIdentityKey(int deviceId, const QByteArray &identityKey); + + bool containsPreKey(); + std::optional loadPreKey(int deviceId, int id); + bool savePreKey(int deviceId, int id, const KeyPair &preKey); + + std::optional loadSignedPreKey(int deviceId); + bool saveSignedPreKey(int deviceId, const SignedPreKey& signedPreKey); + + const QString jid; + + private: + MDB_env *env{}; + MDB_dbi dbiDevices{}; + MDB_dbi dbiKeys{}; + MDB_dbi dbiPreKeys{}; + MDB_dbi dbiIdentityKeys{}; + }; + +} // namespace QXmpp::Omemo diff --git a/qomemo/device.cpp b/qomemo/device.cpp new file mode 100644 index 0000000..edfee1c --- /dev/null +++ b/qomemo/device.cpp @@ -0,0 +1,5 @@ +/* + * Created by victoria on 2021-05-13. + */ + +#include "device.h" diff --git a/qomemo/device.h b/qomemo/device.h new file mode 100644 index 0000000..6a67a19 --- /dev/null +++ b/qomemo/device.h @@ -0,0 +1,25 @@ +/* + * Created by victoria on 2021-05-13. + */ + +#pragma once + +#include + +class QXmppElement; + +class QXmppIq; + +namespace QXmpp::Omemo { + + class Device { + public: + int id; + }; + + class DeviceList { + public: + QList devices; + }; + +} // namespace QXmpp::Omemo diff --git a/qomemo/device_key_storage.cpp b/qomemo/device_key_storage.cpp new file mode 100644 index 0000000..d8c74df --- /dev/null +++ b/qomemo/device_key_storage.cpp @@ -0,0 +1,14 @@ +/* + * Created by victoria on 2021-05-12. + */ + +#include "device_key_storage.h" +#include + +int QXmpp::Omemo::DeviceKeyStorage::generateDeviceId() { + QRandomGenerator random{}; + + return 1 + random.bounded(INT32_MAX - 1); +} + +QXmpp::Omemo::DeviceKeyStorage::DeviceKeyStorage(int deviceId) : deviceId(deviceId) {} diff --git a/qomemo/device_key_storage.h b/qomemo/device_key_storage.h new file mode 100644 index 0000000..efeade9 --- /dev/null +++ b/qomemo/device_key_storage.h @@ -0,0 +1,18 @@ +/* + * Created by victoria on 2021-05-12. + */ + +#pragma once + +namespace QXmpp::Omemo { + + class DeviceKeyStorage { + public: + static int generateDeviceId(); + + explicit DeviceKeyStorage(int deviceId); + + const int deviceId; + }; + +} // namespace QXmpp::Omemo diff --git a/qomemo/device_service.cpp b/qomemo/device_service.cpp new file mode 100644 index 0000000..653a3a6 --- /dev/null +++ b/qomemo/device_service.cpp @@ -0,0 +1,35 @@ +/* + * Created by victoria on 2021-05-12. + */ + +#include "device_service.h" +#include "device.h" + +using namespace QXmpp::Omemo; + +DeviceService::DeviceService(QObject *parent) : QObject(parent) {} + +void DeviceService::onDeviceListReceived(const QString &jid, const QXmpp::Omemo::DeviceList &list) { + + for (const auto &device : list.devices) { + qInfo() << "Got device for" << jid << ":" << device.id; + } +} + +QSharedPointer DeviceService::getDatabase(const QString &jid) { + if (!databases.contains(jid)) { + databases.insert(jid, QSharedPointer::create(jid)); + } + + return databases[jid]; +} + +void DeviceService::addIdentity(const QString &jid, int deviceId, const QByteArray& publicKey) { + auto db = getDatabase(jid); + + db->saveIdentityKey(deviceId, publicKey); +} + +void DeviceService::onDeviceListNotFound(const QString &jid) { + qInfo() << "Device list not found:" << jid; +} diff --git a/qomemo/device_service.h b/qomemo/device_service.h new file mode 100644 index 0000000..2442486 --- /dev/null +++ b/qomemo/device_service.h @@ -0,0 +1,38 @@ +/* + * Created by victoria on 2021-05-12. + */ + +#pragma once + +#include + +#include "qomemo.h" +#include "user_device_list.h" +#include "database.h" + +#include + +namespace QXmpp::Omemo { + + class DeviceList; + + class DeviceService : public QObject { + Q_OBJECT + + public: + explicit DeviceService(QObject *parent); + + QSharedPointer getDatabase(const QString& jid); + + public slots: + void addIdentity(const QString& jid, int deviceId, const QByteArray& publicKey); + + void onDeviceListReceived(const QString &jid, const QXmpp::Omemo::DeviceList &list); + + void onDeviceListNotFound(const QString &jid); + + private: + QMap> databases{}; + }; + +} // namespace QXmpp::Omemo diff --git a/qomemo/key.cpp b/qomemo/key.cpp new file mode 100644 index 0000000..7e9cc30 --- /dev/null +++ b/qomemo/key.cpp @@ -0,0 +1,6 @@ +/* + * Created by victoria on 2021-05-15. +*/ + +#include "key.h" + \ No newline at end of file diff --git a/qomemo/key.h b/qomemo/key.h new file mode 100644 index 0000000..a0910c7 --- /dev/null +++ b/qomemo/key.h @@ -0,0 +1,27 @@ +/* + * Created by victoria on 2021-05-15. +*/ + +#pragma once + +#include + +#include + +namespace QXmpp::Omemo { + + class KeyPair { + public: + QByteArray publicKey{}; + std::optional secretKey{ std::nullopt }; + }; + + class SignedPreKey { + public: + int id{ 0 }; + + KeyPair key{}; + QByteArray signature{}; + }; + +} diff --git a/qomemo/qomemo.cpp b/qomemo/qomemo.cpp new file mode 100644 index 0000000..2b67119 --- /dev/null +++ b/qomemo/qomemo.cpp @@ -0,0 +1,94 @@ +/* + * Created by victoria on 2021-05-12. + */ + +#include "qomemo.h" +#include "sce.h" +#include + +#include +#include +#include + +using namespace QXmpp::Factories; + +QXmppElement QXmpp::Omemo::EncryptedMessage::header() const { + auto result = createElement("header"); + result.setAttribute("sid", QString::number(fromDeviceId)); + + auto ivNode = createElement("iv"); + ivNode.setValue(iv); + + for (const auto &key : keys) { + result.appendChild(key.toXml()); + } + + return result; +} + +QXmppElement QXmpp::Omemo::EncryptedMessage::toXml() const { + auto result = createElement("encrypted", "eu.siacs.conversations.axolotl"); + + result.appendChild(header()); + // TODO: Payload is optional + result.appendChild(payload()); + + return result; +} + +QXmppElement QXmpp::Omemo::EncryptedMessage::payload() const { + QBuffer buffer; + buffer.open(QIODevice::ReadWrite); + QXmlStreamWriter writer(&buffer); + message.toXml(&writer); + + QDomDocument doc; + doc.setContent(buffer.data(), true); + + QXmppElement root(doc.documentElement()); + root.setTagName("payload"); + + return root; +} + +QXmppElement QXmpp::Omemo::EncryptedMessage::content() const { + auto envelope = createElement("content", "urn:xmpp:sce:0"); + + envelope.appendChild(payload()); + + if (!from.isEmpty()) { + auto fromNode = createElement("from"); + fromNode.setAttribute("jid", from); + envelope.appendChild(fromNode); + } + + if (!to.isEmpty()) { + auto toNode = createElement("to"); + toNode.setAttribute("jid", to); + envelope.appendChild(toNode); + } + + if (!timestamp.isNull()) { + auto timeNode = createElement("time"); + timeNode.setAttribute("stamp", timestamp.toString(Qt::DateFormat::ISODate)); + envelope.appendChild(timeNode); + } + + auto rpad = createElement("rpad"); + rpad.setValue(QXmpp::Sce::generatePadding()); + envelope.appendChild(rpad); + + return envelope; +} + +QXmppElement QXmpp::Omemo::MessageKey::toXml() const { + auto result = createElement("key"); + + result.setAttribute("rid", QString::number(receivingDeviceId)); + if (prekey) + result.setAttribute("prekey", "true"); + + result.setValue(key); + + return result; +} diff --git a/qomemo/qomemo.h b/qomemo/qomemo.h new file mode 100644 index 0000000..1e0c71a --- /dev/null +++ b/qomemo/qomemo.h @@ -0,0 +1,42 @@ +/* + * Created by victoria on 2021-05-12. + */ + +#pragma once + +#include + +#include +#include + +namespace QXmpp::Omemo { + + class MessageKey { + public: + [[nodiscard]] QXmppElement toXml() const; + + int receivingDeviceId{}; + bool prekey{}; + QString key{}; + }; + + class EncryptedMessage { + public: + [[nodiscard]] QXmppElement header() const; + [[nodiscard]] QXmppElement content() const; + [[nodiscard]] QXmppElement toXml() const; + [[nodiscard]] QXmppElement payload() const; + + int fromDeviceId{}; + + QList keys{}; + QString from{}; + QString to{}; + QDateTime timestamp{}; + + QString iv{}; + + QXmppMessage message{}; + }; + +} // namespace QXmpp::Omemo diff --git a/qomemo/qxmpp_omemo_manager.cpp b/qomemo/qxmpp_omemo_manager.cpp new file mode 100644 index 0000000..e70754c --- /dev/null +++ b/qomemo/qxmpp_omemo_manager.cpp @@ -0,0 +1,292 @@ +/* + * Created by victoria on 2021-05-12. + */ + +#include "qxmpp_omemo_manager.h" + +#include "qomemo/signal/context.h" + +#include "bundle.h" +#include "device.h" +#include "variant/conversations.h" + +#include + +#include +#include +#include +#include +#include + +using namespace QXmpp::Omemo; + +Manager::Manager() + : deviceService{new DeviceService(this)}, + omemoVariant(new Variant::Conversations), + signalContext(new Signal::Context) { + connect(this, &Manager::deviceListReceived, deviceService.get(), &DeviceService::onDeviceListReceived); + connect(this, &Manager::deviceListNotFound, deviceService.get(), &DeviceService::onDeviceListNotFound); + connect(this, &Manager::deviceListNotFound, this, [this](const QString &jid) { + if (jid == client()->configuration().jidBare()) + generateDeviceListForSelf(); + }); + connect(this, &Manager::deviceListReceived, this, [this](const QString &jid, const DeviceList ¤tList) { + if (jid == client()->configuration().jidBare()) + generateDeviceForSelfIfNeeded(currentList); + }); +} + +bool QXmpp::Omemo::Manager::handleStanza(const QDomElement &stanza) { + QString str{}; + QTextStream info(&str); + stanza.save(info, 4); + + std::cout << str.toStdString(); + + if (handleDeviceList(stanza) || handleMissingDeviceList(stanza) || handleEncryptedMessage(stanza)) + return true; + + return false; +} + +bool Manager::handleDeviceList(const QDomElement &stanza) { + if (!(stanza.tagName() == "iq")) + return false; + + if (!(stanza.attribute("type") == "result")) + return false; + + auto pubsub = stanza.firstChildElement("pubsub"); + if (pubsub.isNull()) + return false; + + auto items = pubsub.firstChildElement("items"); + if (!(items.attribute("node") == omemoVariant->getDeviceListNode())) + return false; + + auto deviceList = omemoVariant->latestDeviceListFromPubSubNode(items); + if (!deviceList.has_value()) + return false; + + emit deviceListReceived(stanza.attribute("from"), deviceList.value()); + + return true; +} + +bool Manager::handleMissingDeviceList(const QDomElement &stanza) { + if (stanza.tagName() != "iq") + return false; + + if (stanza.attribute("type") != "error") + return false; + + auto pubsub = stanza.firstChildElement("pubsub"); + if (pubsub.isNull()) + return false; + + auto items = pubsub.firstChildElement("items"); + if (items.attribute("node") != omemoVariant->getDeviceListNode()) + return false; + + auto error = stanza.firstChildElement("error"); + + if (error.namespaceURI() != "jabber:client" || error.attribute("code") != "404") + return false; + + qDebug() << "Got 404 deviceList for" << stanza.attribute("from"); + emit deviceListNotFound(stanza.attribute("from")); + + return true; +} + +bool Manager::handleEncryptedMessage(const QDomElement &stanza) { + if (stanza.tagName() != "message") + return false; + + auto encrypted = stanza.firstChildElement("encrypted"); + if (encrypted.isNull()) + return false; + + qDebug() << "!!!! Got encrypted message!!"; + + return true; +} + +void QXmpp::Omemo::Manager::setClient(QXmppClient *client) { + QXmppClientExtension::setClient(client); + + if (!client) + return; + + QObject::connect(client, &QXmppClient::connected, this, + &Manager::fetchOwnDevices); +} + +void QXmpp::Omemo::Manager::fetchOwnDevices() { + QXmppPubSubIq iq{}; + iq.setFrom(client()->configuration().jid()); + iq.setTo(client()->configuration().jidBare()); + iq.setType(QXmppIq::Get); + iq.setQueryNode(omemoVariant->getDeviceListNode()); + + client()->sendPacket(iq); +} + +QSharedPointer Manager::getDeviceService() { + return deviceService; +} + +void Manager::publishDeviceList(const DeviceList &deviceList) { +// QXmppPubSubIq iq{}; +// iq.setFrom(client()->configuration().jid()); +// iq.setType(QXmppIq::Set); +// iq.setQueryNode(omemoVariant->getDeviceListNode()); +// iq.setItems() + + auto iq = omemoVariant->deviceListSetIq(deviceList); + iq.setFrom(client()->configuration().jid()); + + QString str{}; + QXmlStreamWriter info(&str); + iq.toXml(&info); + qInfo() << str; + + client()->sendPacket(iq); +} + +void Manager::generateDeviceListForSelf() { + qInfo() << "Generate device for self..."; + + generateDeviceForSelfIfNeeded(DeviceList()); +} + +void Manager::publishBundle(int deviceId, const Bundle &bundle) { + auto iq = omemoVariant->bundleSetIq(deviceId, bundle); + iq.setFrom(client()->configuration().jid()); + + QString str{}; + QXmlStreamWriter info(&str); + iq.toXml(&info); + qInfo() << str; + + client()->sendPacket(iq); +} + +void Manager::generateDeviceForSelfIfNeeded(const DeviceList ¤tList) { + auto db = deviceService->getDatabase(client()->configuration().jidBare()); + auto activeId = db->loadActiveDeviceId(); + + if (activeId.has_value()) { + qInfo() << "Current device:" << *activeId; + + bool found = false; + for (const auto &d : currentList.devices) + if (d.id == *activeId) + found = true; + + if (found) + return; + + qInfo() << "Could not find device" << *activeId << ", generating new one"; + } + + qInfo() << "Generating device"; + + auto deviceId = QRandomGenerator64::system()->bounded(INT32_MAX); + db->saveActiveDeviceId(deviceId); + + Device device{}; + device.id = deviceId; + + auto updatedList = currentList; + + + updatedList.devices.push_back(device); + publishDeviceList(updatedList); + + auto bundle = generateAndSaveBundle(deviceId); + publishBundle(deviceId, bundle); +} + +Bundle Manager::generateAndSaveBundle(int deviceId) { + auto database = deviceService->getDatabase(client()->configuration().jidBare()); + Bundle result{}; + + ec_key_pair *ecKeyPair; + curve_generate_key_pair(signalContext->temporaryGetContextUnsafeForRawAccessThatNeedsToBeWrapped(), &ecKeyPair); + + signal_buffer *ecPublic; + ec_public_key_serialize(&ecPublic, ec_key_pair_get_public(ecKeyPair)); + + signal_buffer *ecSecret; + ec_private_key_serialize(&ecSecret, ec_key_pair_get_private(ecKeyPair)); + + QByteArray identityKey((const char *) signal_buffer_const_data(ecPublic), (int) signal_buffer_len(ecPublic)); + QByteArray identityKeySecret((const char *) signal_buffer_const_data(ecSecret), (int) signal_buffer_len(ecSecret)); + + database->saveIdentityKey(deviceId, identityKey); + database->saveIdentityKeySecret(deviceId, identityKeySecret); + + result.ik = identityKey; + + // Generate SPK + ec_key_pair *ecSpk; + curve_generate_key_pair(signalContext->temporaryGetContextUnsafeForRawAccessThatNeedsToBeWrapped(), &ecSpk); + signal_buffer *spkPublic, *spkSecret; + ec_public_key_serialize(&spkPublic, ec_key_pair_get_public(ecSpk)); + ec_private_key_serialize(&spkSecret, ec_key_pair_get_private(ecSpk)); + + // Generate SPKs + signal_buffer *signature; + curve_calculate_signature(signalContext->temporaryGetContextUnsafeForRawAccessThatNeedsToBeWrapped(), &signature, + ec_key_pair_get_private(ecKeyPair), signal_buffer_const_data(spkPublic), + signal_buffer_len(spkPublic)); + + SignedPreKey spk{}; + spk.id = 1; + spk.key.publicKey = QByteArray((const char *) signal_buffer_const_data(spkPublic), (int) signal_buffer_len(spkPublic)); + spk.key.secretKey = QByteArray((const char *) signal_buffer_const_data(spkSecret), (int) signal_buffer_len(spkSecret)); + spk.signature = QByteArray((const char *) signal_buffer_const_data(signature), (int) signal_buffer_len(signature)); + + result.spk = spk.key.publicKey; + result.spks = spk.signature; + result.spkId = 1; + + // Generate 100 PK + for (auto i = 1; i <= 100; ++i) { + ec_key_pair *currentPreKey; + curve_generate_key_pair(signalContext->temporaryGetContextUnsafeForRawAccessThatNeedsToBeWrapped(), ¤tPreKey); + + signal_buffer *pkPublic, *pkSecret; + ec_public_key_serialize(&pkPublic, ec_key_pair_get_public(currentPreKey)); + ec_private_key_serialize(&pkSecret, ec_key_pair_get_private(currentPreKey)); + + KeyPair preKey{}; + preKey.publicKey = QByteArray((const char *) signal_buffer_const_data(pkPublic), (int) signal_buffer_len(pkPublic)); + preKey.secretKey = QByteArray((const char *) signal_buffer_const_data(pkSecret), (int) signal_buffer_len(pkSecret)); + + database->savePreKey(deviceId, i, preKey); + + PreKey pk{}; + pk.data = preKey.publicKey; + pk.id = i; + + result.prekeys.append(pk); + + signal_buffer_free(pkPublic); + signal_buffer_free(pkSecret); + SIGNAL_UNREF(currentPreKey); + } + + signal_buffer_free(signature); + + signal_buffer_free(ecPublic); + signal_buffer_free(ecSecret); + SIGNAL_UNREF(ecKeyPair); + + signal_buffer_free(spkPublic); + signal_buffer_free(spkSecret); + SIGNAL_UNREF(ecSpk); + + return result; +} diff --git a/qomemo/qxmpp_omemo_manager.h b/qomemo/qxmpp_omemo_manager.h new file mode 100644 index 0000000..03c2aa2 --- /dev/null +++ b/qomemo/qxmpp_omemo_manager.h @@ -0,0 +1,64 @@ +/* + * Created by victoria on 2021-05-12. + */ + +#pragma once + +#include "device_service.h" +#include "qomemo.h" +#include "variant/omemo_base.h" + +#include +#include + +namespace Signal { + class Context; +} + +namespace QXmpp::Omemo { + + class Manager : public QXmppClientExtension { + Q_OBJECT; + + public: + Manager(); + ~Manager() override = default; + + bool handleStanza(const QDomElement &stanza) override; + + bool handleDeviceList(const QDomElement& stanza); + + bool handleMissingDeviceList(const QDomElement& stanza); + + bool handleEncryptedMessage(const QDomElement& stanza); + + QSharedPointer getDeviceService(); + + Bundle generateAndSaveBundle(int deviceId); + + public slots: + void fetchOwnDevices(); + + void publishDeviceList(const QXmpp::Omemo::DeviceList& deviceList); + + void generateDeviceListForSelf(); + + void generateDeviceForSelfIfNeeded(const QXmpp::Omemo::DeviceList ¤tList); + + void publishBundle(int deviceId, const QXmpp::Omemo::Bundle& bundle); + + signals: + void deviceListReceived(const QString &jid, const QXmpp::Omemo::DeviceList &list); + + void deviceListNotFound(const QString &jid); + + protected: + void setClient(QXmppClient *client) override; + + private: + QSharedPointer deviceService; + QScopedPointer omemoVariant; + std::shared_ptr signalContext; + }; + +} // namespace QXmpp::Omemo diff --git a/qomemo/sce.cpp b/qomemo/sce.cpp new file mode 100644 index 0000000..b94cede --- /dev/null +++ b/qomemo/sce.cpp @@ -0,0 +1,26 @@ +/* + * Created by victoria on 2021-05-12. + */ + +#include "sce.h" + +#include + +#define RPAD_ALPHABET "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~" + +constexpr int RPAD_MAX_LENGTH = 200; + +QString QXmpp::Sce::generatePadding() { + QRandomGenerator random{}; + QString result{}; + QString alphabet{QStringLiteral(RPAD_ALPHABET)}; + + auto length = random.bounded(RPAD_MAX_LENGTH); + result.resize(length); + + for (auto i = 0; i < length; ++i) { + result[i] = alphabet[random.bounded(alphabet.length())]; + } + + return result; +} diff --git a/qomemo/sce.h b/qomemo/sce.h new file mode 100644 index 0000000..497aa3d --- /dev/null +++ b/qomemo/sce.h @@ -0,0 +1,14 @@ +/* + * Created by victoria on 2021-05-12. + */ + +#pragma once + +#include +#include + +namespace QXmpp::Sce { + + QString generatePadding(); + +} diff --git a/qomemo/signal/CMakeLists.txt b/qomemo/signal/CMakeLists.txt new file mode 100644 index 0000000..7223904 --- /dev/null +++ b/qomemo/signal/CMakeLists.txt @@ -0,0 +1,9 @@ +target_sources(squawk PRIVATE + context.cpp + context.h + util.cpp + util.h + ) + +add_subdirectory(crypto) +add_subdirectory(stores) \ No newline at end of file diff --git a/qomemo/signal/context.cpp b/qomemo/signal/context.cpp new file mode 100644 index 0000000..75d3557 --- /dev/null +++ b/qomemo/signal/context.cpp @@ -0,0 +1,28 @@ +/* + * Created by victoria on 2021-05-13. + */ + +#include "context.h" +#include "crypto/crypto.h" + +using namespace Signal; + +Context::Context() : cryptoProvider{ Signal::Crypto::createProvider() } { + signal_context_create(&ctx, nullptr); + signal_context_set_crypto_provider(ctx, &cryptoProvider); +} + +Context::~Context() { + signal_context_destroy(ctx); +} + +std::unique_ptr Context::generateCurveKeyPair() { + auto result = std::unique_ptr(); + // TODO + + return result; +} + +signal_context *Context::temporaryGetContextUnsafeForRawAccessThatNeedsToBeWrapped() { + return ctx; +} diff --git a/qomemo/signal/context.h b/qomemo/signal/context.h new file mode 100644 index 0000000..30941d9 --- /dev/null +++ b/qomemo/signal/context.h @@ -0,0 +1,32 @@ +/* + * Created by victoria on 2021-05-13. + */ + +#pragma once + +#include "crypto/ec.h" + +#include + +#include + +namespace Signal { + + class Context { + public: + Context(); + ~Context(); + Context(const Context &) = delete; + Context(Context &&) = delete; + Context &operator=(const Context &) = delete; + + std::unique_ptr generateCurveKeyPair(); + + signal_context *temporaryGetContextUnsafeForRawAccessThatNeedsToBeWrapped(); + + private: + signal_crypto_provider cryptoProvider{}; + signal_context *ctx{nullptr}; + }; + +} // namespace Signal diff --git a/qomemo/signal/crypto/CMakeLists.txt b/qomemo/signal/crypto/CMakeLists.txt new file mode 100644 index 0000000..8f43895 --- /dev/null +++ b/qomemo/signal/crypto/CMakeLists.txt @@ -0,0 +1,12 @@ +target_sources(squawk PRIVATE + aes_openssl.cpp + aes_openssl.h + crypto.cpp + crypto.h + hmac_sha256_openssl.cpp + hmac_sha256_openssl.h + sha512_digest_openssl.cpp + sha512_digest_openssl.h + ec.cpp + ec.h + ) diff --git a/qomemo/signal/crypto/aes_openssl.cpp b/qomemo/signal/crypto/aes_openssl.cpp new file mode 100644 index 0000000..17f2df8 --- /dev/null +++ b/qomemo/signal/crypto/aes_openssl.cpp @@ -0,0 +1,187 @@ +/* + * Created by victoria on 2021-05-13. + */ + +#include "aes_openssl.h" +#include + +extern "C" { +#include +#include +} + +using namespace Signal::Crypto; + +class EVPCipherCtxWrapper { +public: + EVPCipherCtxWrapper() { +#if OPENSSL_VERSION_NUMBER >= 0x1010000fL + ctx = EVP_CIPHER_CTX_new(); +#else + ctx = new EVP_CIPHER_CTX; + EVP_CIPHER_CTX_init(ctx); +#endif + } + + ~EVPCipherCtxWrapper() { + if (good()) { +#if OPENSSL_VERSION_NUMBER >= 0x1010000fL + EVP_CIPHER_CTX_free(ctx); +#else + EVP_CIPHER_CTX_cleanup(ctx); + delete ctx; +#endif + } + } + + EVPCipherCtxWrapper(const EVPCipherCtxWrapper &) = delete; + EVPCipherCtxWrapper(EVPCipherCtxWrapper &&) = delete; + EVPCipherCtxWrapper &operator=(const EVPCipherCtxWrapper &) = delete; + + [[nodiscard]] bool good() const { return ctx != nullptr; } + + [[nodiscard]] EVP_CIPHER_CTX *operator*() const { return ctx; } + +private: + EVP_CIPHER_CTX *ctx{nullptr}; +}; + +static const EVP_CIPHER *aes_cipher(int cipher, size_t key_len) { + if (cipher == SG_CIPHER_AES_CBC_PKCS5) { + if (key_len == 16) { + return EVP_aes_128_cbc(); + } else if (key_len == 24) { + return EVP_aes_192_cbc(); + } else if (key_len == 32) { + return EVP_aes_256_cbc(); + } + } else if (cipher == SG_CIPHER_AES_CTR_NOPADDING) { + if (key_len == 16) { + return EVP_aes_128_ctr(); + } else if (key_len == 24) { + return EVP_aes_192_ctr(); + } else if (key_len == 32) { + return EVP_aes_256_ctr(); + } + } + return nullptr; +} + +int Aes::encrypt(signal_buffer **output, int cipher, const uint8_t *key, size_t key_len, const uint8_t *iv, + size_t iv_len, const uint8_t *plaintext, size_t plaintext_len, void *) { + const EVP_CIPHER *evp_cipher = aes_cipher(cipher, key_len); + if (!evp_cipher) { + fprintf(stderr, "invalid AES mode or key size: %zu\n", key_len); + return SG_ERR_UNKNOWN; + } + + if (iv_len != 16) { + fprintf(stderr, "invalid AES IV size: %zu\n", iv_len); + return SG_ERR_UNKNOWN; + } + + if (plaintext_len > INT_MAX - EVP_CIPHER_block_size(evp_cipher)) { + fprintf(stderr, "invalid plaintext length: %zu\n", plaintext_len); + return SG_ERR_UNKNOWN; + } + + EVPCipherCtxWrapper ctx{}; + if (!ctx.good()) { + fprintf(stderr, "could not create context\n"); + return SG_ERR_UNKNOWN; + } + + auto result = EVP_EncryptInit_ex(*ctx, evp_cipher, nullptr, key, iv); + if (!result) { + fprintf(stderr, "cannot initialize cipher\n"); + return SG_ERR_UNKNOWN; + } + + if (cipher == SG_CIPHER_AES_CTR_NOPADDING) { + result = EVP_CIPHER_CTX_set_padding(*ctx, 0); + if (!result) { + fprintf(stderr, "cannot set padding\n"); + return SG_ERR_UNKNOWN; + } + } + + auto out_buf = std::make_unique(plaintext_len + EVP_CIPHER_block_size(evp_cipher)); + + int out_len = 0; + result = EVP_EncryptUpdate(*ctx, out_buf.get(), &out_len, plaintext, + plaintext_len); + if (!result) { + fprintf(stderr, "cannot encrypt plaintext\n"); + return SG_ERR_UNKNOWN; + } + + int final_len = 0; + result = EVP_EncryptFinal_ex(*ctx, out_buf.get() + out_len, &final_len); + if (!result) { + fprintf(stderr, "cannot finish encrypting plaintext\n"); + return SG_ERR_UNKNOWN; + } + + *output = signal_buffer_create(out_buf.get(), out_len + final_len); + + return result; +} + +int Aes::decrypt(signal_buffer **output, int cipher, const uint8_t *key, size_t key_len, const uint8_t *iv, + size_t iv_len, const uint8_t *ciphertext, size_t ciphertext_len, void *) { + const EVP_CIPHER *evp_cipher = aes_cipher(cipher, key_len); + if (!evp_cipher) { + fprintf(stderr, "invalid AES mode or key size: %zu\n", key_len); + return SG_ERR_INVAL; + } + + if (iv_len != 16) { + fprintf(stderr, "invalid AES IV size: %zu\n", iv_len); + return SG_ERR_INVAL; + } + + if (ciphertext_len > INT_MAX - EVP_CIPHER_block_size(evp_cipher)) { + fprintf(stderr, "invalid ciphertext length: %zu\n", ciphertext_len); + return SG_ERR_UNKNOWN; + } + + EVPCipherCtxWrapper ctx{}; + if (!ctx.good()) { + fprintf(stderr, "could not create context\n"); + return SG_ERR_UNKNOWN; + } + + auto result = EVP_DecryptInit_ex(*ctx, evp_cipher, nullptr, key, iv); + if (!result) { + fprintf(stderr, "cannot initialize cipher\n"); + return SG_ERR_UNKNOWN; + } + + if (cipher == SG_CIPHER_AES_CTR_NOPADDING) { + result = EVP_CIPHER_CTX_set_padding(*ctx, 0); + if (!result) { + fprintf(stderr, "cannot set padding\n"); + return SG_ERR_UNKNOWN; + } + } + + auto out_buf = std::make_unique(ciphertext_len + EVP_CIPHER_block_size(evp_cipher)); + + int out_len = 0; + result = EVP_DecryptUpdate(*ctx, out_buf.get(), &out_len, ciphertext, ciphertext_len); + if (!result) { + fprintf(stderr, "cannot decrypt ciphertext\n"); + return SG_ERR_UNKNOWN; + } + + int final_len = 0; + result = EVP_DecryptFinal_ex(*ctx, out_buf.get() + out_len, &final_len); + if (!result) { + fprintf(stderr, "cannot finish decrypting ciphertext\n"); + return SG_ERR_UNKNOWN; + } + + *output = signal_buffer_create(out_buf.get(), out_len + final_len); + + return result; +} \ No newline at end of file diff --git a/qomemo/signal/crypto/aes_openssl.h b/qomemo/signal/crypto/aes_openssl.h new file mode 100644 index 0000000..63f4635 --- /dev/null +++ b/qomemo/signal/crypto/aes_openssl.h @@ -0,0 +1,17 @@ +/* + * Created by victoria on 2021-05-13. + */ + +#pragma once + +#include + +namespace Signal::Crypto::Aes { + + int encrypt(signal_buffer **output, int cipher, const uint8_t *key, size_t key_len, const uint8_t *iv, + size_t iv_len, const uint8_t *plaintext, size_t plaintext_len, void *user_data); + + int decrypt(signal_buffer **output, int cipher, const uint8_t *key, size_t key_len, const uint8_t *iv, + size_t iv_len, const uint8_t *ciphertext, size_t ciphertext_len, void *user_data); + +} // namespace Signal::Crypto::Aes diff --git a/qomemo/signal/crypto/crypto.cpp b/qomemo/signal/crypto/crypto.cpp new file mode 100644 index 0000000..d6a6058 --- /dev/null +++ b/qomemo/signal/crypto/crypto.cpp @@ -0,0 +1,40 @@ +/* + * Created by victoria on 2021-05-13. + */ + +#include "crypto.h" + +extern "C" { +#include +} + +#include "aes_openssl.h" +#include "hmac_sha256_openssl.h" +#include "sha512_digest_openssl.h" + +int random_func(uint8_t *data, size_t len, void *) { + if (RAND_bytes(data, len)) { + return 0; + } else { + return SG_ERR_UNKNOWN; + } +} + +signal_crypto_provider Signal::Crypto::createProvider() { + signal_crypto_provider result{}; + + result.random_func = random_func; + result.hmac_sha256_init_func = HmacSha256::init; + result.hmac_sha256_update_func = HmacSha256::update; + result.hmac_sha256_final_func = HmacSha256::final; + result.hmac_sha256_cleanup_func = HmacSha256::cleanup; + result.sha512_digest_init_func = Sha512::init; + result.sha512_digest_update_func = Sha512::update; + result.sha512_digest_final_func = Sha512::final; + result.sha512_digest_cleanup_func = Sha512::cleanup; + result.encrypt_func = Aes::encrypt; + result.decrypt_func = Aes::decrypt; + result.user_data = nullptr; + + return result; +} diff --git a/qomemo/signal/crypto/crypto.h b/qomemo/signal/crypto/crypto.h new file mode 100644 index 0000000..2fbff31 --- /dev/null +++ b/qomemo/signal/crypto/crypto.h @@ -0,0 +1,13 @@ +/* + * Created by victoria on 2021-05-13. + */ + +#pragma once + +#include + +namespace Signal::Crypto { + + signal_crypto_provider createProvider(); + +} diff --git a/qomemo/signal/crypto/ec.cpp b/qomemo/signal/crypto/ec.cpp new file mode 100644 index 0000000..875d2ee --- /dev/null +++ b/qomemo/signal/crypto/ec.cpp @@ -0,0 +1,5 @@ +/* + * Created by victoria on 2021-06-17. +*/ + +#include "ec.h" diff --git a/qomemo/signal/crypto/ec.h b/qomemo/signal/crypto/ec.h new file mode 100644 index 0000000..280d00a --- /dev/null +++ b/qomemo/signal/crypto/ec.h @@ -0,0 +1,20 @@ +/* + * Created by victoria on 2021-06-17. +*/ + +#pragma once + +#include + +namespace Signal::Crypto { + + class ECKeyPair { + public: + ECKeyPair(); + ~ECKeyPair(); + + private: + ec_key_pair *ec; + }; + +} diff --git a/qomemo/signal/crypto/hmac_sha256_openssl.cpp b/qomemo/signal/crypto/hmac_sha256_openssl.cpp new file mode 100644 index 0000000..2a8aba5 --- /dev/null +++ b/qomemo/signal/crypto/hmac_sha256_openssl.cpp @@ -0,0 +1,71 @@ +/* + * Created by victoria on 2021-05-13. + */ + +#include "hmac_sha256_openssl.h" + +extern "C" { +#include +#include +#include +} + +using namespace Signal::Crypto; + +int HmacSha256::init(void **hmac_context, const uint8_t *key, size_t key_len, void *) { +#if OPENSSL_VERSION_NUMBER >= 0x1010000fL + HMAC_CTX *ctx = HMAC_CTX_new(); + if (!ctx) { + return SG_ERR_NOMEM; + } +#else + auto ctx = new HMAC_CTX; + HMAC_CTX_init(ctx); +#endif + + *hmac_context = ctx; + + if (HMAC_Init_ex(ctx, key, key_len, EVP_sha256(), nullptr) != 1) { + return SG_ERR_UNKNOWN; + } + + return SG_SUCCESS; +} + +int HmacSha256::update(void *hmac_context, const uint8_t *data, size_t data_len, void *) { + auto ctx = static_cast(hmac_context); + int result = HMAC_Update(ctx, data, data_len); + + return (result == 1) ? SG_SUCCESS : SG_ERR_UNKNOWN; +} + +int HmacSha256::final(void *hmac_context, signal_buffer **output, void *) { + auto ctx = static_cast(hmac_context); + unsigned char md[EVP_MAX_MD_SIZE]; + unsigned int len = 0; + + if (HMAC_Final(ctx, md, &len) != 1) { + return SG_ERR_UNKNOWN; + } + + signal_buffer *output_buffer = signal_buffer_create(md, len); + if (!output_buffer) { + return SG_ERR_NOMEM; + } + + *output = output_buffer; + + return SG_SUCCESS; +} + +void HmacSha256::cleanup(void *hmac_context, void *) { + if (hmac_context) { + auto ctx = static_cast(hmac_context); +#if OPENSSL_VERSION_NUMBER >= 0x1010000fL + HMAC_CTX_free(ctx); +#else + HMAC_CTX_cleanup(ctx); + delete ctx; +#endif + } +} \ No newline at end of file diff --git a/qomemo/signal/crypto/hmac_sha256_openssl.h b/qomemo/signal/crypto/hmac_sha256_openssl.h new file mode 100644 index 0000000..03fe8e0 --- /dev/null +++ b/qomemo/signal/crypto/hmac_sha256_openssl.h @@ -0,0 +1,16 @@ +/* + * Created by victoria on 2021-05-13. + */ + +#pragma once + +#include + +namespace Signal::Crypto::HmacSha256 { + + int init(void **hmac_context, const uint8_t *key, size_t key_len, void *); + int update(void *hmac_context, const uint8_t *data, size_t data_len, void *); + int final(void *hmac_context, signal_buffer **output, void *); + void cleanup(void *hmac_context, void *); + +} // namespace Signal::Crypto::HmacSha256 diff --git a/qomemo/signal/crypto/sha512_digest_openssl.cpp b/qomemo/signal/crypto/sha512_digest_openssl.cpp new file mode 100644 index 0000000..360f11e --- /dev/null +++ b/qomemo/signal/crypto/sha512_digest_openssl.cpp @@ -0,0 +1,67 @@ +/* + * Created by victoria on 2021-05-13. + */ + +#include "sha512_digest_openssl.h" + +extern "C" { +#include +#include +#include +} + +using namespace Signal::Crypto; + +int Sha512::init(void **digest_context, void *) { + auto ctx = EVP_MD_CTX_create(); + if (!ctx) { + return SG_ERR_NOMEM; + } + + auto result = EVP_DigestInit_ex(ctx, EVP_sha512(), nullptr); + + if (result == 1) { + *digest_context = ctx; + return SG_SUCCESS; + } + + EVP_MD_CTX_destroy(ctx); + return SG_ERR_UNKNOWN; +} + +int Sha512::update(void *digest_context, const uint8_t *data, size_t data_len, void *) { + auto ctx = static_cast(digest_context); + auto result = EVP_DigestUpdate(ctx, data, data_len); + + return (result == 1) ? SG_SUCCESS : SG_ERR_UNKNOWN; +} + +int Sha512::final(void *digest_context, signal_buffer **output, void *) { + auto ctx = static_cast(digest_context); + unsigned char md[EVP_MAX_MD_SIZE]; + unsigned int len = 0; + + auto result = EVP_DigestFinal_ex(ctx, md, &len); + if (result != 1) { + return SG_ERR_UNKNOWN; + } + + result = EVP_DigestInit_ex(ctx, EVP_sha512(), nullptr); + if (result != 1) { + return SG_ERR_UNKNOWN; + } + + signal_buffer *output_buffer = signal_buffer_create(md, len); + if (!output_buffer) { + return SG_ERR_NOMEM; + } + + *output = output_buffer; + + return SG_SUCCESS; +} + +void Sha512::cleanup(void *digest_context, void *) { + auto ctx = static_cast(digest_context); + EVP_MD_CTX_destroy(ctx); +} \ No newline at end of file diff --git a/qomemo/signal/crypto/sha512_digest_openssl.h b/qomemo/signal/crypto/sha512_digest_openssl.h new file mode 100644 index 0000000..8b263a2 --- /dev/null +++ b/qomemo/signal/crypto/sha512_digest_openssl.h @@ -0,0 +1,16 @@ +/* + * Created by victoria on 2021-05-13. + */ + +#pragma once + +#include + +namespace Signal::Crypto::Sha512 { + + int init(void **digest_context, void *); + int update(void *digest_context, const uint8_t *data, size_t data_len, void *); + int final(void *digest_context, signal_buffer **output, void *); + void cleanup(void *digest_context, void *); + +} // namespace Signal::Crypto::Sha512 diff --git a/qomemo/signal/stores/CMakeLists.txt b/qomemo/signal/stores/CMakeLists.txt new file mode 100644 index 0000000..971b87b --- /dev/null +++ b/qomemo/signal/stores/CMakeLists.txt @@ -0,0 +1,14 @@ +target_sources(squawk PRIVATE + identity_key_store.cpp + identity_key_store.h + pre_key_store.cpp + pre_key_store.h + sender_key_store.cpp + sender_key_store.h + session_store.cpp + session_store.h + signed_pre_key_store.cpp + signed_pre_key_store.h + store_context.cpp + store_context.h + ) \ No newline at end of file diff --git a/qomemo/signal/stores/identity_key_store.cpp b/qomemo/signal/stores/identity_key_store.cpp new file mode 100644 index 0000000..e80b7d3 --- /dev/null +++ b/qomemo/signal/stores/identity_key_store.cpp @@ -0,0 +1,70 @@ +/* + * Created by victoria on 2021-05-13. + */ + +#include "identity_key_store.h" + +#include "qomemo/signal/util.h" +#include + +Signal::Store::IdentityKeyStore::IdentityKeyStore(QXmpp::Omemo::DeviceService &deviceService, QString jid, int deviceId) + : jid(std::move(jid)), deviceId(deviceId), deviceService(deviceService) { + database = deviceService.getDatabase(jid); +} + +int Signal::Store::IdentityKeyStore::getIdentityKeyPair(signal_buffer **public_data, signal_buffer **private_data) { + auto pk = database->loadIdentityKey(deviceId); + auto sk = database->loadIdentityKeySecret(deviceId); + + *public_data = signal_buffer_create(reinterpret_cast(pk->data()), pk->size()); + *private_data = signal_buffer_create(reinterpret_cast(sk->data()), sk->size()); + + return 0; +} + +int Signal::Store::IdentityKeyStore::getLocalRegistrationId(uint32_t *registration_id) { + // TODO: Figure out what registration id is used for + *registration_id = 1; + + return 0; +} + +int Signal::Store::IdentityKeyStore::saveIdentity(const signal_protocol_address *address, uint8_t *key_data, + size_t key_len) { + auto identityJid = Signal::Util::jidFromAddress(address); + auto identityKey = Signal::Util::byteArray(key_data, key_len); + + deviceService.getDatabase(identityJid)->saveIdentityKey(address->device_id, identityKey); + + return 0; +} + +int Signal::Store::IdentityKeyStore::isTrustedIdentity(const signal_protocol_address *address, uint8_t *key_data, + size_t key_len) { + auto identityJid = Signal::Util::jidFromAddress(address); + auto actualIdentityKey = deviceService.getDatabase(identityJid)->loadIdentityKey(address->device_id); + + if (!actualIdentityKey.has_value()) { + return 1; + } + + auto givenIdentityKey = Signal::Util::byteArray(key_data, key_len); + + return givenIdentityKey == actualIdentityKey ? 1 : 0; +} + +void Signal::Store::IdentityKeyStore::fillCallbacks(signal_protocol_identity_key_store &store) { + store.get_identity_key_pair = [](signal_buffer **public_data, signal_buffer **private_data, void *ptr) { + return static_cast(ptr)->getIdentityKeyPair(public_data, private_data); + }; + store.get_local_registration_id = [](void *ptr, uint32_t *registrationId) { + return static_cast(ptr)->getLocalRegistrationId(registrationId); + }; + store.is_trusted_identity = [](const signal_protocol_address *address, uint8_t *key_data, size_t key_len, + void *ptr) { + return static_cast(ptr)->isTrustedIdentity(address, key_data, key_len); + }; + store.save_identity = [](const signal_protocol_address *address, uint8_t *key_data, size_t key_len, void *ptr) { + return static_cast(ptr)->saveIdentity(address, key_data, key_len); + }; +} diff --git a/qomemo/signal/stores/identity_key_store.h b/qomemo/signal/stores/identity_key_store.h new file mode 100644 index 0000000..017ca42 --- /dev/null +++ b/qomemo/signal/stores/identity_key_store.h @@ -0,0 +1,32 @@ +/* + * Created by victoria on 2021-05-13. + */ + +#pragma once + +#include + +#include "qomemo/device_service.h" + +namespace Signal::Store { + + class IdentityKeyStore { + public: + IdentityKeyStore(QXmpp::Omemo::DeviceService &deviceService, QString jid, int deviceId); + + int getIdentityKeyPair(signal_buffer **public_data, signal_buffer **private_data); + int getLocalRegistrationId(uint32_t *registration_id); + int saveIdentity(const signal_protocol_address *address, uint8_t *key_data, size_t key_len); + int isTrustedIdentity(const signal_protocol_address *address, uint8_t *key_data, size_t key_len); + + void fillCallbacks(signal_protocol_identity_key_store &store); + + const QString jid; + const int deviceId; + + private: + QXmpp::Omemo::DeviceService &deviceService; + QSharedPointer database; + }; + +} // namespace Signal::Store diff --git a/qomemo/signal/stores/pre_key_store.cpp b/qomemo/signal/stores/pre_key_store.cpp new file mode 100644 index 0000000..72bfe72 --- /dev/null +++ b/qomemo/signal/stores/pre_key_store.cpp @@ -0,0 +1,38 @@ +/* + * Created by victoria on 2021-05-13. + */ + +#include "pre_key_store.h" + +Signal::Store::PreKeyStore::PreKeyStore(QSharedPointer database) : database( + std::move(database)) {} + +int Signal::Store::PreKeyStore::containsPreKey(uint32_t pre_key_id) { + return 0; +} + +int Signal::Store::PreKeyStore::loadPreKey(signal_buffer **record, uint32_t pre_key_id) { + return 0; +} + +int Signal::Store::PreKeyStore::storePreKey(uint32_t pre_key_id, uint8_t *record, size_t record_len) { + return 0; +} + +int Signal::Store::PreKeyStore::removePreKey(uint32_t pre_key_id) { return 0; } + +void Signal::Store::PreKeyStore::fillCallbacks(signal_protocol_pre_key_store &store) { + store.contains_pre_key = [](uint32_t id, void *ptr) { + return static_cast(ptr)->containsPreKey(id); + }; + store.load_pre_key = [](signal_buffer **record, uint32_t id, void *ptr) { + return static_cast(ptr)->loadPreKey(record, id); + }; + store.remove_pre_key = [](uint32_t id, void *ptr) { + return static_cast(ptr)->removePreKey(id); + }; + store.store_pre_key = [](uint32_t id, uint8_t *record, size_t size, + void *ptr) { + return static_cast(ptr)->storePreKey(id, record, size); + }; +} diff --git a/qomemo/signal/stores/pre_key_store.h b/qomemo/signal/stores/pre_key_store.h new file mode 100644 index 0000000..0ec1a79 --- /dev/null +++ b/qomemo/signal/stores/pre_key_store.h @@ -0,0 +1,28 @@ +/* + * Created by victoria on 2021-05-13. + */ + +#pragma once + +#include + +#include "qomemo/device_service.h" + +namespace Signal::Store { + + class PreKeyStore { + public: + explicit PreKeyStore(QSharedPointer database); + + int containsPreKey(uint32_t pre_key_id); + int loadPreKey(signal_buffer **record, uint32_t pre_key_id); + int storePreKey(uint32_t pre_key_id, uint8_t *record, size_t record_len); + int removePreKey(uint32_t pre_key_id); + + void fillCallbacks(signal_protocol_pre_key_store &store); + + private: + QSharedPointer database; + }; + +} // namespace Signal::Store diff --git a/qomemo/signal/stores/sender_key_store.cpp b/qomemo/signal/stores/sender_key_store.cpp new file mode 100644 index 0000000..2db5231 --- /dev/null +++ b/qomemo/signal/stores/sender_key_store.cpp @@ -0,0 +1,30 @@ +/* + * Created by victoria on 2021-05-13. + */ + +#include "sender_key_store.h" + +int Signal::Store::SenderKeyStore::loadSenderKey(signal_buffer **record, signal_buffer **user_record, + const signal_protocol_sender_key_name *sender_key_name) { + return 0; +} + +int Signal::Store::SenderKeyStore::storeSenderKey(const signal_protocol_sender_key_name *sender_key_name, + uint8_t *record, size_t record_len, uint8_t *user_record, + size_t user_record_len) { + return 0; +} + +void Signal::Store::SenderKeyStore::fillCallbacks(signal_protocol_sender_key_store &store) { + store.user_data = nullptr; + store.destroy_func = nullptr; + store.load_sender_key = [](signal_buffer **record, signal_buffer **user_record, + const signal_protocol_sender_key_name *sender_key_name, void *ptr) { + return static_cast(ptr)->loadSenderKey(record, user_record, sender_key_name); + }; + store.store_sender_key = [](const signal_protocol_sender_key_name *sender_key_name, uint8_t *record, + size_t record_len, uint8_t *user_record, size_t user_record_len, void *ptr) { + return static_cast(ptr)->storeSenderKey(sender_key_name, record, record_len, user_record, + user_record_len); + }; +} diff --git a/qomemo/signal/stores/sender_key_store.h b/qomemo/signal/stores/sender_key_store.h new file mode 100644 index 0000000..a5abcc6 --- /dev/null +++ b/qomemo/signal/stores/sender_key_store.h @@ -0,0 +1,21 @@ +/* + * Created by victoria on 2021-05-13. + */ + +#pragma once + +#include + +namespace Signal::Store { + + class SenderKeyStore { + public: + int storeSenderKey(const signal_protocol_sender_key_name *sender_key_name, uint8_t *record, size_t record_len, + uint8_t *user_record, size_t user_record_len); + int loadSenderKey(signal_buffer **record, signal_buffer **user_record, + const signal_protocol_sender_key_name *sender_key_name); + + void fillCallbacks(signal_protocol_sender_key_store &store); + }; + +} // namespace Signal::Store diff --git a/qomemo/signal/stores/session_store.cpp b/qomemo/signal/stores/session_store.cpp new file mode 100644 index 0000000..4b9a5f9 --- /dev/null +++ b/qomemo/signal/stores/session_store.cpp @@ -0,0 +1,57 @@ +/* + * Created by victoria on 2021-05-13. + */ + +#include "session_store.h" + +int Signal::Store::SessionStore::loadSession(signal_buffer **record, signal_buffer **user_record, + const signal_protocol_address *address) { + return 0; +} + +int Signal::Store::SessionStore::getSubDeviceSessions(signal_int_list **sessions, const char *name, size_t name_len) { + return 0; +} + +int Signal::Store::SessionStore::storeSession(const signal_protocol_address *address, uint8_t *record, + size_t record_len, uint8_t *user_record, size_t user_record_len) { + return 0; +} + +int Signal::Store::SessionStore::containsSession(const signal_protocol_address *address) { + return 0; +} + +int Signal::Store::SessionStore::deleteSession(const signal_protocol_address *address) { + return 0; +} + +int Signal::Store::SessionStore::deleteAllSessions(const char *name, size_t name_len) { + return 0; +} + +void Signal::Store::SessionStore::fillCallbacks(signal_protocol_session_store &store) { + store.user_data = nullptr; + store.destroy_func = nullptr; + store.load_session_func = [](signal_buffer **record, signal_buffer **user_record, + const signal_protocol_address *address, void *ptr) { + return static_cast(ptr)->loadSession(record, user_record, address); + }; + store.get_sub_device_sessions_func = [](signal_int_list **sessions, const char *name, size_t name_len, void *ptr) { + return static_cast(ptr)->getSubDeviceSessions(sessions, name, name_len); + }; + store.store_session_func = [](const signal_protocol_address *address, uint8_t *record, size_t record_len, + uint8_t *user_record, size_t user_record_len, void *ptr) { + return static_cast(ptr)->storeSession(address, record, record_len, user_record, + user_record_len); + }; + store.contains_session_func = [](const signal_protocol_address *address, void *ptr) { + return static_cast(ptr)->containsSession(address); + }; + store.delete_session_func = [](const signal_protocol_address *address, void *ptr) { + return static_cast(ptr)->deleteSession(address); + }; + store.delete_all_sessions_func = [](const char *name, size_t name_len, void *ptr) { + return static_cast(ptr)->deleteAllSessions(name, name_len); + }; +} diff --git a/qomemo/signal/stores/session_store.h b/qomemo/signal/stores/session_store.h new file mode 100644 index 0000000..c773c83 --- /dev/null +++ b/qomemo/signal/stores/session_store.h @@ -0,0 +1,24 @@ +/* + * Created by victoria on 2021-05-13. + */ + +#pragma once + +#include + +namespace Signal::Store { + + class SessionStore { + public: + int loadSession(signal_buffer **record, signal_buffer **user_record, const signal_protocol_address *address); + int getSubDeviceSessions(signal_int_list **sessions, const char *name, size_t name_len); + int storeSession(const signal_protocol_address *address, uint8_t *record, size_t record_len, + uint8_t *user_record, size_t user_record_len); + int containsSession(const signal_protocol_address *address); + int deleteSession(const signal_protocol_address *address); + int deleteAllSessions(const char *name, size_t name_len); + + void fillCallbacks(signal_protocol_session_store &store); + }; + +} // namespace Signal::Store diff --git a/qomemo/signal/stores/signed_pre_key_store.cpp b/qomemo/signal/stores/signed_pre_key_store.cpp new file mode 100644 index 0000000..483465d --- /dev/null +++ b/qomemo/signal/stores/signed_pre_key_store.cpp @@ -0,0 +1,43 @@ +/* + * Created by victoria on 2021-05-13. + */ + +#include "signed_pre_key_store.h" + +int Signal::Store::SignedPreKeyStore::loadSignedPreKey(signal_buffer **record, uint32_t signed_pre_key_id) { + return 0; +} + +int +Signal::Store::SignedPreKeyStore::storeSignedPreKey(uint32_t signed_pre_key_id, uint8_t *record, size_t record_len) { + return 0; +} + +int Signal::Store::SignedPreKeyStore::containsSignedPreKey(uint32_t signed_pre_key_id) { + return 0; +} + +int Signal::Store::SignedPreKeyStore::removeSignedPreKey(uint32_t signed_pre_key_id) { + return 0; +} + +void Signal::Store::SignedPreKeyStore::fillCallbacks(signal_protocol_signed_pre_key_store &store) { + store.user_data = nullptr; + store.destroy_func = nullptr; + store.load_signed_pre_key = [](signal_buffer **record, uint32_t signed_pre_key_id, void *ptr) { + return static_cast(ptr)->loadSignedPreKey( + record, signed_pre_key_id); + }; + store.store_signed_pre_key = [](uint32_t signed_pre_key_id, uint8_t *record, size_t record_len, void *ptr) { + return static_cast(ptr)->storeSignedPreKey( + signed_pre_key_id, record, record_len); + }; + store.contains_signed_pre_key = [](uint32_t signed_pre_key_id, void *ptr) { + return static_cast(ptr)->containsSignedPreKey( + signed_pre_key_id); + }; + store.remove_signed_pre_key = [](uint32_t signed_pre_key_id, void *ptr) { + return static_cast(ptr)->removeSignedPreKey( + signed_pre_key_id); + }; +} diff --git a/qomemo/signal/stores/signed_pre_key_store.h b/qomemo/signal/stores/signed_pre_key_store.h new file mode 100644 index 0000000..2544741 --- /dev/null +++ b/qomemo/signal/stores/signed_pre_key_store.h @@ -0,0 +1,21 @@ +/* + * Created by victoria on 2021-05-13. + */ + +#pragma once + +#include + +namespace Signal::Store { + + class SignedPreKeyStore { + public: + int loadSignedPreKey(signal_buffer **record, uint32_t signed_pre_key_id); + int storeSignedPreKey(uint32_t signed_pre_key_id, uint8_t *record, size_t record_len); + int containsSignedPreKey(uint32_t signed_pre_key_id); + int removeSignedPreKey(uint32_t signed_pre_key_id); + + void fillCallbacks(signal_protocol_signed_pre_key_store &store); + }; + +} // namespace Signal::Store diff --git a/qomemo/signal/stores/store_context.cpp b/qomemo/signal/stores/store_context.cpp new file mode 100644 index 0000000..e4dd58a --- /dev/null +++ b/qomemo/signal/stores/store_context.cpp @@ -0,0 +1,25 @@ +/* + * Created by victoria on 2021-05-13. + */ + +#include "store_context.h" + +Signal::Store::Context::Context(signal_context *global) : identityKeyStore(), preKeyStore(), senderKeyStore(), sessionStore(), signedPreKeyStore() { + signal_protocol_store_context_create(&ctx, global); + + identityKeyStore->fillCallbacks(iks); + preKeyStore->fillCallbacks(pks); + senderKeyStore->fillCallbacks(sks); + sessionStore->fillCallbacks(ss); + signedPreKeyStore->fillCallbacks(spks); + + signal_protocol_store_context_set_identity_key_store(ctx, &iks); + signal_protocol_store_context_set_pre_key_store(ctx, &pks); + signal_protocol_store_context_set_sender_key_store(ctx, &sks); + signal_protocol_store_context_set_session_store(ctx, &ss); + signal_protocol_store_context_set_signed_pre_key_store(ctx, &spks); +} + +Signal::Store::Context::~Context() { + signal_protocol_store_context_destroy(ctx); +} diff --git a/qomemo/signal/stores/store_context.h b/qomemo/signal/stores/store_context.h new file mode 100644 index 0000000..7c1ea55 --- /dev/null +++ b/qomemo/signal/stores/store_context.h @@ -0,0 +1,42 @@ +/* + * Created by victoria on 2021-05-13. + */ + +#pragma once + +#include + +#include "identity_key_store.h" +#include "pre_key_store.h" +#include "sender_key_store.h" +#include "session_store.h" +#include "signed_pre_key_store.h" + +namespace Signal::Store { + + class Context { + public: + explicit Context(signal_context *global); + ~Context(); + + Context(const Context &) = delete; + Context(Context &&) = delete; + Context &operator=(const Context &) = delete; + + private: + signal_protocol_store_context *ctx{nullptr}; + + signal_protocol_identity_key_store iks{}; + signal_protocol_pre_key_store pks{}; + signal_protocol_sender_key_store sks{}; + signal_protocol_session_store ss{}; + signal_protocol_signed_pre_key_store spks{}; + + std::unique_ptr identityKeyStore; + std::unique_ptr preKeyStore; + std::unique_ptr senderKeyStore; + std::unique_ptr sessionStore; + std::unique_ptr signedPreKeyStore; + }; + +} // namespace Signal::Store diff --git a/qomemo/signal/util.cpp b/qomemo/signal/util.cpp new file mode 100644 index 0000000..9c2ed08 --- /dev/null +++ b/qomemo/signal/util.cpp @@ -0,0 +1,14 @@ +/* + * Created by victoria on 2021-05-15. +*/ + +#include "util.h" + +QString Signal::Util::jidFromAddress(const signal_protocol_address *address) { + // TODO: Validate this + return QString::fromRawData(reinterpret_cast(address->name), static_cast(address->name_len)); +} + +QByteArray Signal::Util::byteArray(const uint8_t *data, size_t len) { + return QByteArray::fromRawData(reinterpret_cast(data), static_cast(len)); +} diff --git a/qomemo/signal/util.h b/qomemo/signal/util.h new file mode 100644 index 0000000..e1145ef --- /dev/null +++ b/qomemo/signal/util.h @@ -0,0 +1,18 @@ +/* + * Created by victoria on 2021-05-15. +*/ + +#pragma once + +#include + +#include +#include + +namespace Signal::Util { + + QString jidFromAddress(const signal_protocol_address *address); + + QByteArray byteArray(const uint8_t *data, size_t len); + +} diff --git a/qomemo/user_device_list.cpp b/qomemo/user_device_list.cpp new file mode 100644 index 0000000..5910ed2 --- /dev/null +++ b/qomemo/user_device_list.cpp @@ -0,0 +1,9 @@ +/* + * Created by victoria on 2021-05-12. + */ + +#include "user_device_list.h" + +#include + +QXmpp::Omemo::UserDeviceList::UserDeviceList(QString jid) : jid(std::move(jid)) {} diff --git a/qomemo/user_device_list.h b/qomemo/user_device_list.h new file mode 100644 index 0000000..00465aa --- /dev/null +++ b/qomemo/user_device_list.h @@ -0,0 +1,18 @@ +/* + * Created by victoria on 2021-05-12. + */ + +#pragma once + +#include + +namespace QXmpp::Omemo { + + class UserDeviceList { + public: + explicit UserDeviceList(QString jid); + + const QString jid; + }; + +} // namespace QXmpp::Omemo diff --git a/qomemo/variant/CMakeLists.txt b/qomemo/variant/CMakeLists.txt new file mode 100644 index 0000000..a2b211a --- /dev/null +++ b/qomemo/variant/CMakeLists.txt @@ -0,0 +1 @@ +target_sources(squawk PRIVATE xep0384.cpp xep0384.h conversations.cpp conversations.h omemo_base.cpp omemo_base.h) diff --git a/qomemo/variant/conversations.cpp b/qomemo/variant/conversations.cpp new file mode 100644 index 0000000..1658c37 --- /dev/null +++ b/qomemo/variant/conversations.cpp @@ -0,0 +1,205 @@ +/* + * Created by victoria on 2021-05-13. + */ + +#include "conversations.h" + +#include "qomemo/bundle.h" +#include "qomemo/device.h" +#include "shared/qxmppfactories.h" + +#include +#include +#include + +using namespace QXmpp::Omemo; +using namespace QXmpp::Factories; + +QXmppElement Variant::Conversations::deviceToXml(const Device &device) { + auto result = createElement("device"); + result.setAttribute("id", QString::number(device.id)); + + return result; +} + +Device Variant::Conversations::deviceFromXml(const QXmppElement &xml) { + Device result{}; + + if (!elementMatches(xml, "device")) + return result; + + result.id = xml.attribute("id").toInt(); + + return result; +} + +QXmppElement +Variant::Conversations::deviceListToXml(const DeviceList &deviceList) { + auto element = createElement("list", "eu.siacs.conversations.axolotl"); + + for (const auto &device : deviceList.devices) { + element.appendChild(deviceToXml(device)); + } + + return element; +} + +std::optional Variant::Conversations::latestDeviceListFromPubSubNode(const QXmppElement &items) { + auto item = items.firstChildElement("item"); + if (item.isNull()) + return std::nullopt; + + auto list = item.firstChildElement("list"); + if (list.isNull()) + return std::nullopt; + + if (!elementMatches(list, "list", "eu.siacs.conversations.axolotl")) + return std::nullopt; + + DeviceList result{}; + + auto deviceElement = list.firstChildElement("device"); + while (!deviceElement.isNull()) { + result.devices.push_back(deviceFromXml(deviceElement)); + deviceElement = deviceElement.nextSiblingElement("device"); + } + + return result; +} + +QXmppIq Variant::Conversations::deviceListSetIq(const DeviceList &deviceList) { + QXmppIq iq{}; + + iq.setType(QXmppIq::Set); + + auto item = createElement("item"); + item.appendChild(deviceListToXml(deviceList)); + + auto publish = createElement("publish"); + publish.setAttribute("node", getDeviceListNode()); + publish.appendChild(item); + + auto pubSub = createElement("pubsub", "http://jabber.org/protocol/pubsub"); + pubSub.appendChild(publish); + pubSub.appendChild(createOpenPublishOptions()); + + iq.setExtensions({ pubSub }); + + return iq; +} + +QString Variant::Conversations::getDeviceListNode() const { + return "eu.siacs.conversations.axolotl.devicelist"; +} + +QXmppIq Variant::Conversations::bundleSetIq(int deviceId, const Bundle &bundle) { + QXmppIq iq{}; + + iq.setType(QXmppIq::Set); + + auto item = createElement("item"); + item.appendChild(bundleToXml(bundle)); + + auto publish = createElement("publish"); + publish.setAttribute( + "node", + QString("eu.siacs.conversations.axolotl.bundles:%1").arg(deviceId)); + publish.appendChild(item); + + auto pubSub = createElement("pubsub", "http://jabber.org/protocol/pubsub"); + pubSub.appendChild(publish); + pubSub.appendChild(createOpenPublishOptions()); + + iq.setExtensions({ pubSub }); + + return iq; +} + +QXmppElement Variant::Conversations::bundleToXml(const Bundle &bundle) { + auto spkNode = createElement("signedPreKeyPublic"); + spkNode.setAttribute("signedPreKeyId", QString::number(bundle.spkId)); + spkNode.setValue(bundle.spk.toBase64()); + + auto spksNode = createElement("signedPreKeySignature"); + spksNode.setValue(bundle.spks.toBase64()); + + auto ikNode = createElement("identityKey"); + ikNode.setValue(bundle.ik.toBase64()); + + auto prekeysNode = createElement("prekeys"); + for (const auto &pk : bundle.prekeys) { + prekeysNode.appendChild(preKeyToXml(pk)); + } + + auto result = createElement("bundle", "eu.siacs.conversations.axolotl"); + result.appendChild(spkNode); + result.appendChild(spksNode); + result.appendChild(ikNode); + result.appendChild(prekeysNode); + + return result; +} + +QXmppElement Variant::Conversations::preKeyToXml(const PreKey &pk) { + auto x = createElement("preKeyPublic"); + x.setAttribute("preKeyId", QString::number(pk.id)); + x.setValue(pk.data.toBase64()); + + return x; +} + +std::optional Variant::Conversations::preKeyFromXml(const QXmppElement &xml) { + if (!elementMatches(xml, "preKeyPublic")) + return std::nullopt; + + auto pk = PreKey(); + pk.id = xml.attribute("preKeyId").toInt(); + pk.data = QByteArray::fromBase64Encoding(xml.value().toUtf8()).decoded; + + return pk; +} + +std::optional Variant::Conversations::bundleFromXml(const QXmppElement &xml) { + if (!elementMatches(xml, "bundle", "eu.siacs.conversations.axolotl")) + return std::nullopt; + + auto spkNode = xml.firstChildElement("signedPreKeyPublic"); + if (spkNode.isNull()) { + qWarning() << "'bundle': missing 'signedPreKeyPublic'"; + return std::nullopt; + } + + Bundle result{}; + + result.spk = QByteArray::fromBase64Encoding(spkNode.value().toUtf8()).decoded; + result.spkId = spkNode.attribute("signedPreKeyId").toInt(); + + auto spksNode = xml.firstChildElement("signedPreKeySignature"); + if (spksNode.isNull()) { + qWarning() << "'bundle': missing 'signedPreKeySignature'"; + return std::nullopt; + } + + result.spks = QByteArray::fromBase64Encoding(spksNode.value().toUtf8()).decoded; + + auto ikNode = xml.firstChildElement("identityKey"); + if (ikNode.isNull()) { + qWarning() << "'bundle': missing 'identityKey'"; + return std::nullopt; + } + + result.ik = QByteArray::fromBase64Encoding(ikNode.value().toUtf8()).decoded; + + auto prekeysNode = xml.firstChildElement("prekeys"); + auto pkNode = prekeysNode.firstChildElement("preKeyPublic"); + + result.prekeys.clear(); + while (!pkNode.isNull()) { + auto maybePk = preKeyFromXml(pkNode); + if (maybePk) + result.prekeys.push_back(*maybePk); + pkNode = pkNode.nextSiblingElement("preKeyPublic"); + } + + return result; +} diff --git a/qomemo/variant/conversations.h b/qomemo/variant/conversations.h new file mode 100644 index 0000000..5a467f0 --- /dev/null +++ b/qomemo/variant/conversations.h @@ -0,0 +1,32 @@ +/* + * Created by victoria on 2021-05-13. + */ + +#pragma once + +#include "omemo_base.h" + +namespace QXmpp::Omemo::Variant { + + class Conversations : public Base { + public: + ~Conversations() override = default; + + QXmppElement deviceToXml(const Device &device) override; + Device deviceFromXml(const QXmppElement &xml) override; + + [[nodiscard]] QString getDeviceListNode() const override; + QXmppElement deviceListToXml(const DeviceList &deviceList) override; + std::optional latestDeviceListFromPubSubNode(const QXmppElement &items) override; + QXmppIq deviceListSetIq(const DeviceList &deviceList) override; + + QXmppElement preKeyToXml(const PreKey &pk) override; + std::optional preKeyFromXml(const QXmppElement &xml) override; + + QXmppElement bundleToXml(const Bundle &bundle) override; + std::optional bundleFromXml(const QXmppElement &xml) override; + + QXmppIq bundleSetIq(int deviceId, const Bundle &bundle) override; + }; + +} // namespace QXmpp::Omemo::Variant diff --git a/qomemo/variant/omemo_base.cpp b/qomemo/variant/omemo_base.cpp new file mode 100644 index 0000000..a3e8493 --- /dev/null +++ b/qomemo/variant/omemo_base.cpp @@ -0,0 +1,5 @@ +/* + * Created by victoria on 2021-05-13. + */ + +#include "omemo_base.h" diff --git a/qomemo/variant/omemo_base.h b/qomemo/variant/omemo_base.h new file mode 100644 index 0000000..88d2a9f --- /dev/null +++ b/qomemo/variant/omemo_base.h @@ -0,0 +1,45 @@ +/* + * Created by victoria on 2021-05-13. + */ + +#pragma once + +#include + +class QString; +class QXmppElement; +class QXmppIq; + +namespace QXmpp::Omemo { + + class PreKey; + class Bundle; + class Device; + class DeviceList; + + namespace Variant { + + class Base { + public: + virtual ~Base() = default; + + virtual QXmppElement deviceToXml(const Device &device) = 0; + virtual Device deviceFromXml(const QXmppElement &xml) = 0; + + [[nodiscard]] virtual QString getDeviceListNode() const = 0; + virtual QXmppElement deviceListToXml(const DeviceList &deviceList) = 0; + virtual std::optional latestDeviceListFromPubSubNode(const QXmppElement &xml) = 0; + virtual QXmppIq deviceListSetIq(const DeviceList &deviceList) = 0; + + virtual QXmppElement preKeyToXml(const PreKey &pk) = 0; + virtual std::optional preKeyFromXml(const QXmppElement &xml) = 0; + + virtual QXmppElement bundleToXml(const Bundle& bundle) = 0; + virtual std::optional bundleFromXml(const QXmppElement& xml) = 0; + + virtual QXmppIq bundleSetIq(int deviceId, const Bundle& bundle) = 0; + }; + + } // namespace Variant + +} // namespace QXmpp::Omemo diff --git a/qomemo/variant/xep0384.cpp b/qomemo/variant/xep0384.cpp new file mode 100644 index 0000000..8ec2597 --- /dev/null +++ b/qomemo/variant/xep0384.cpp @@ -0,0 +1,5 @@ +/* + * Created by victoria on 2021-05-13. + */ + +#include "xep0384.h" diff --git a/qomemo/variant/xep0384.h b/qomemo/variant/xep0384.h new file mode 100644 index 0000000..9298b24 --- /dev/null +++ b/qomemo/variant/xep0384.h @@ -0,0 +1,7 @@ +/* + * Created by victoria on 2021-05-13. + */ + +#pragma once + +namespace QXmpp::Omemo {} diff --git a/resources/CMakeLists.txt b/resources/CMakeLists.txt new file mode 100644 index 0000000..86433f3 --- /dev/null +++ b/resources/CMakeLists.txt @@ -0,0 +1,14 @@ +target_sources(squawk PRIVATE resources.qrc) + +configure_file(images/logo.svg squawk.svg COPYONLY) + +execute_process(COMMAND convert -background none -size 48x48 squawk.svg squawk48.png WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) +execute_process(COMMAND convert -background none -size 64x64 squawk.svg squawk64.png WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) +execute_process(COMMAND convert -background none -size 128x128 squawk.svg squawk128.png WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) +execute_process(COMMAND convert -background none -size 256x256 squawk.svg squawk256.png WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) + +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/squawk.svg DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/scalable/apps) +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/squawk48.png DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/48x48/apps RENAME squawk.png) +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/squawk64.png DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/64x64/apps RENAME squawk.png) +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/squawk128.png DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/128x128/apps RENAME squawk.png) +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/squawk256.png DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/256x256/apps RENAME squawk.png) diff --git a/resources/images/fallback/dark/big/document-preview.svg b/resources/images/fallback/dark/big/document-preview.svg new file mode 100644 index 0000000..49a3feb --- /dev/null +++ b/resources/images/fallback/dark/big/document-preview.svg @@ -0,0 +1,11 @@ + + + + + + + diff --git a/resources/images/fallback/dark/big/folder.svg b/resources/images/fallback/dark/big/folder.svg new file mode 100644 index 0000000..2acb4ab --- /dev/null +++ b/resources/images/fallback/dark/big/folder.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/images/fallback/dark/small/document-preview.svg b/resources/images/fallback/dark/small/document-preview.svg new file mode 100644 index 0000000..43d19bf --- /dev/null +++ b/resources/images/fallback/dark/small/document-preview.svg @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/resources/images/fallback/dark/small/folder.svg b/resources/images/fallback/dark/small/folder.svg new file mode 100644 index 0000000..1061f4d --- /dev/null +++ b/resources/images/fallback/dark/small/folder.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/resources/images/fallback/light/big/document-preview.svg b/resources/images/fallback/light/big/document-preview.svg new file mode 100644 index 0000000..6f6e346 --- /dev/null +++ b/resources/images/fallback/light/big/document-preview.svg @@ -0,0 +1,11 @@ + + + + + + + diff --git a/resources/images/fallback/light/big/folder.svg b/resources/images/fallback/light/big/folder.svg new file mode 100644 index 0000000..2acb4ab --- /dev/null +++ b/resources/images/fallback/light/big/folder.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/images/fallback/light/small/document-preview.svg b/resources/images/fallback/light/small/document-preview.svg new file mode 100644 index 0000000..f40fcdf --- /dev/null +++ b/resources/images/fallback/light/small/document-preview.svg @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/resources/images/fallback/light/small/folder.svg b/resources/images/fallback/light/small/folder.svg new file mode 100644 index 0000000..a5f66cd --- /dev/null +++ b/resources/images/fallback/light/small/folder.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/resources/resources.qrc b/resources/resources.qrc index 4fb3e5b..58565fc 100644 --- a/resources/resources.qrc +++ b/resources/resources.qrc @@ -40,6 +40,8 @@ images/fallback/dark/big/favorite.svg images/fallback/dark/big/unfavorite.svg images/fallback/dark/big/add.svg + images/fallback/dark/big/folder.svg + images/fallback/dark/big/document-preview.svg images/fallback/dark/small/absent.svg @@ -80,6 +82,8 @@ images/fallback/dark/small/favorite.svg images/fallback/dark/small/unfavorite.svg images/fallback/dark/small/add.svg + images/fallback/dark/small/folder.svg + images/fallback/dark/small/document-preview.svg images/fallback/light/big/absent.svg @@ -120,6 +124,8 @@ images/fallback/light/big/favorite.svg images/fallback/light/big/unfavorite.svg images/fallback/light/big/add.svg + images/fallback/light/big/folder.svg + images/fallback/light/big/document-preview.svg images/fallback/light/small/absent.svg @@ -160,5 +166,7 @@ images/fallback/light/small/favorite.svg images/fallback/light/small/unfavorite.svg images/fallback/light/small/add.svg + images/fallback/light/small/folder.svg + images/fallback/light/small/document-preview.svg diff --git a/shared/CMakeLists.txt b/shared/CMakeLists.txt new file mode 100644 index 0000000..97ed619 --- /dev/null +++ b/shared/CMakeLists.txt @@ -0,0 +1,21 @@ +target_sources(squawk PRIVATE + enums.h + global.cpp + global.h + exception.cpp + exception.h + icons.cpp + icons.h + message.cpp + message.h + messageinfo.cpp + messageinfo.h + order.h + shared.h + utils.cpp + utils.h + vcard.cpp + vcard.h + qxmppfactories.cpp + qxmppfactories.h + ) diff --git a/exception.cpp b/shared/exception.cpp similarity index 100% rename from exception.cpp rename to shared/exception.cpp diff --git a/exception.h b/shared/exception.h similarity index 100% rename from exception.h rename to shared/exception.h diff --git a/shared/global.cpp b/shared/global.cpp index a6b7b60..25a1c87 100644 --- a/shared/global.cpp +++ b/shared/global.cpp @@ -23,6 +23,11 @@ Shared::Global* Shared::Global::instance = 0; const std::set Shared::Global::supportedImagesExts = {"png", "jpg", "webp", "jpeg", "gif", "svg"}; +#ifdef WITH_KIO +QLibrary Shared::Global::openFileManagerWindowJob("openFileManagerWindowJob"); +Shared::Global::HighlightInFileManager Shared::Global::hfm = 0; +#endif + Shared::Global::Global(): availability({ tr("Online", "Availability"), @@ -80,16 +85,65 @@ Shared::Global::Global(): tr("Your password is going to be stored in KDE wallet storage (KWallet). You're going to be queried for permissions", "AccountPasswordDescription") }), pluginSupport({ - {"KWallet", false} - }) + {"KWallet", false}, + {"openFileManagerWindowJob", false} + }), + fileCache() { if (instance != 0) { throw 551; } instance = this; + +#ifdef WITH_KIO + openFileManagerWindowJob.load(); + if (openFileManagerWindowJob.isLoaded()) { + hfm = (HighlightInFileManager) openFileManagerWindowJob.resolve("highlightInFileManager"); + if (hfm) { + setSupported("openFileManagerWindowJob", true); + qDebug() << "KIO::OpenFileManagerWindow support enabled"; + } else { + qDebug() << "KIO::OpenFileManagerWindow support disabled: couldn't resolve required methods in the library"; + } + } else { + qDebug() << "KIO::OpenFileManagerWindow support disabled: couldn't load the library" << openFileManagerWindowJob.errorString(); + } +#endif } + +static const QSize defaultIconFileInfoHeight(50, 50); +Shared::Global::FileInfo Shared::Global::getFileInfo(const QString& path) +{ + std::map::const_iterator itr = instance->fileCache.find(path); + if (itr == instance->fileCache.end()) { + QMimeDatabase db; + QMimeType type = db.mimeTypeForFile(path); + QStringList parts = type.name().split("/"); + QString big = parts.front(); + QFileInfo info(path); + + FileInfo::Preview p = FileInfo::Preview::none; + QSize size; + if (big == "image") { + if (parts.back() == "gif") { + //TODO need to consider GIF as a movie + } + p = FileInfo::Preview::picture; + QImage img(path); + size = img.size(); + } else { + size = defaultIconFileInfoHeight; + } + + itr = instance->fileCache.insert(std::make_pair(path, FileInfo({info.fileName(), size, type, p}))).first; + } + + return itr->second; +} + + Shared::Global * Shared::Global::getInstance() { return instance; @@ -152,6 +206,69 @@ QString Shared::Global::getDescription(Shared::AccountPassword ap) return instance->accountPasswordDescription[static_cast(ap)]; } + +static const QStringList query = {"query", "default", "inode/directory"}; +static const QRegularExpression dolphinReg("[Dd]olphin"); +static const QRegularExpression nautilusReg("[Nn]autilus"); +static const QRegularExpression cajaReg("[Cc]aja"); +static const QRegularExpression nemoReg("[Nn]emo"); +static const QRegularExpression konquerorReg("kfmclient"); +static const QRegularExpression pcmanfmQtReg("pcmanfm-qt"); +static const QRegularExpression pcmanfmReg("pcmanfm"); +static const QRegularExpression thunarReg("thunar"); + +void Shared::Global::highlightInFileManager(const QString& path) +{ +#ifdef WITH_KIO + if (supported("openFileManagerWindowJob")) { + hfm(path); + return; + } else { + qDebug() << "requested to highlight in file manager url" << path << "but it's not supported: KIO plugin isn't loaded, trying fallback"; + } +#else + qDebug() << "requested to highlight in file manager url" << path << "but it's not supported: squawk wasn't compiled to support it, trying fallback"; +#endif + + QFileInfo info = path; + if (info.exists()) { + QProcess proc; + proc.start("xdg-mime", query); + proc.waitForFinished(); + QString output = proc.readLine().simplified(); + + QString folder; + if (info.isDir()) { + folder = info.canonicalFilePath(); + } else { + folder = info.canonicalPath(); + } + + if (output.contains(dolphinReg)) { + //there is a bug on current (21.04.0) dolphin, it works correct only if you already have dolphin launched + proc.startDetached("dolphin", QStringList() << "--select" << info.canonicalFilePath()); + //KIO::highlightInFileManager({QUrl(info.canonicalFilePath())}); + } else if (output.contains(nautilusReg)) { + proc.startDetached("nautilus", QStringList() << "--select" << info.canonicalFilePath()); //this worked on nautilus + } else if (output.contains(cajaReg)) { + proc.startDetached("caja", QStringList() << folder); //caja doesn't seem to support file selection command line, gonna just open directory + } else if (output.contains(nemoReg)) { + proc.startDetached("nemo", QStringList() << info.canonicalFilePath()); //nemo supports selecting files without keys + } else if (output.contains(konquerorReg)) { + proc.startDetached("konqueror", QStringList() << "--select" << info.canonicalFilePath()); //this worked on konqueror + } else if (output.contains(pcmanfmQtReg)) { + proc.startDetached("pcmanfm-qt", QStringList() << folder); //pcmanfm-qt doesn't seem to support open with selection, gonna just open directory + } else if (output.contains(pcmanfmReg)) { + proc.startDetached("pcmanfm", QStringList() << folder); //pcmanfm also doesn't seem to support open with selection, gonna just open directory + } else if (output.contains(thunarReg)) { + proc.startDetached("thunar", QStringList() << folder); //thunar doesn't seem to support open with selection, gonna just open directory + } else { + QDesktopServices::openUrl(QUrl::fromLocalFile(folder)); + } + } +} + + #define FROM_INT_INPL(Enum) \ template<> \ Enum Shared::Global::fromInt(int src) \ diff --git a/shared/global.h b/shared/global.h index 54e1584..b6bbe37 100644 --- a/shared/global.h +++ b/shared/global.h @@ -29,6 +29,17 @@ #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include namespace Shared { @@ -36,6 +47,19 @@ namespace Shared { Q_DECLARE_TR_FUNCTIONS(Global) public: + struct FileInfo { + enum class Preview { + none, + picture, + movie + }; + + QString name; + QSize size; + QMimeType mime; + Preview preview; + }; + Global(); static Global* getInstance(); @@ -64,6 +88,9 @@ namespace Shared { static const std::set supportedImagesExts; + static FileInfo getFileInfo(const QString& path); + static void highlightInFileManager(const QString& path); + template static T fromInt(int src); @@ -87,6 +114,15 @@ namespace Shared { static Global* instance; std::map pluginSupport; + std::map fileCache; + +#ifdef WITH_KIO + static QLibrary openFileManagerWindowJob; + + typedef void (*HighlightInFileManager)(const QUrl &); + + static HighlightInFileManager hfm; +#endif }; } diff --git a/shared/icons.h b/shared/icons.h index 48ecc37..540d3e9 100644 --- a/shared/icons.h +++ b/shared/icons.h @@ -170,6 +170,8 @@ static const std::map> icons = { {"favorite", {"favorite", "favorite"}}, {"unfavorite", {"draw-star", "unfavorite"}}, {"list-add", {"list-add", "add"}}, + {"folder", {"folder", "folder"}}, + {"document-preview", {"document-preview", "document-preview"}} }; } diff --git a/shared/message.cpp b/shared/message.cpp index af4f9e0..e6b47b2 100644 --- a/shared/message.cpp +++ b/shared/message.cpp @@ -36,7 +36,8 @@ Shared::Message::Message(Shared::Message::Type p_type): errorText(), originalMessage(), lastModified(), - stanzaId() + stanzaId(), + attachPath() {} Shared::Message::Message(): @@ -56,7 +57,8 @@ Shared::Message::Message(): errorText(), originalMessage(), lastModified(), - stanzaId() + stanzaId(), + attachPath() {} QString Shared::Message::getBody() const @@ -311,6 +313,7 @@ void Shared::Message::serialize(QDataStream& data) const data << lastModified; } data << stanzaId; + data << attachPath; } void Shared::Message::deserialize(QDataStream& data) @@ -341,6 +344,7 @@ void Shared::Message::deserialize(QDataStream& data) data >> lastModified; } data >> stanzaId; + data >> attachPath; } bool Shared::Message::change(const QMap& data) @@ -350,6 +354,16 @@ bool Shared::Message::change(const QMap& data) setState(static_cast(itr.value().toUInt())); } + itr = data.find("outOfBandUrl"); + if (itr != data.end()) { + setOutOfBandUrl(itr.value().toString()); + } + + itr = data.find("attachPath"); + if (itr != data.end()) { + setAttachPath(itr.value().toString()); + } + if (state == State::error) { itr = data.find("errorText"); if (itr != data.end()) { @@ -380,18 +394,29 @@ bool Shared::Message::change(const QMap& data) itr = data.find("body"); if (itr != data.end()) { - QMap::const_iterator dItr = data.find("stamp"); - QDateTime correctionDate; - if (dItr != data.end()) { - correctionDate = dItr.value().toDateTime(); - } else { - correctionDate = QDateTime::currentDateTimeUtc(); //in case there is no information about time of this correction it's applied + QString b = itr.value().toString(); + if (body != b) { + QMap::const_iterator dItr = data.find("stamp"); + QDateTime correctionDate; + if (dItr != data.end()) { + correctionDate = dItr.value().toDateTime(); + } else { + correctionDate = QDateTime::currentDateTimeUtc(); //in case there is no information about time of this correction it's applied + } + if (!edited || lastModified < correctionDate) { + originalMessage = body; + lastModified = correctionDate; + setBody(body); + setEdited(true); + } } - if (!edited || lastModified < correctionDate) { - originalMessage = body; - lastModified = correctionDate; - setBody(itr.value().toString()); - setEdited(true); + } else { + QMap::const_iterator dItr = data.find("stamp"); + if (dItr != data.end()) { + QDateTime ntime = dItr.value().toDateTime(); + if (time != ntime) { + setTime(ntime); + } } } @@ -420,7 +445,7 @@ void Shared::Message::setOutOfBandUrl(const QString& url) bool Shared::Message::storable() const { - return id.size() > 0 && (body.size() > 0 || oob.size()) > 0; + return id.size() > 0 && (body.size() > 0 || oob.size() > 0 || attachPath.size() > 0); } void Shared::Message::setStanzaId(const QString& sid) @@ -432,3 +457,33 @@ QString Shared::Message::getStanzaId() const { return stanzaId; } + +QString Shared::Message::getAttachPath() const +{ + return attachPath; +} + +void Shared::Message::setAttachPath(const QString& path) +{ + attachPath = path; +} + +Shared::Message::Change::Change(const QMap& _data): + data(_data), + idModified(false) {} + +void Shared::Message::Change::operator()(Shared::Message& msg) +{ + idModified = msg.change(data); +} + +void Shared::Message::Change::operator()(Shared::Message* msg) +{ + idModified = msg->change(data); +} + +bool Shared::Message::Change::hasIdBeenModified() const +{ + return idModified; +} + diff --git a/shared/message.h b/shared/message.h index d84053f..aa91af6 100644 --- a/shared/message.h +++ b/shared/message.h @@ -16,15 +16,15 @@ * along with this program. If not, see . */ +#ifndef SHAPER_MESSAGE_H +#define SHAPER_MESSAGE_H + #include #include #include #include #include -#ifndef SHAPER_MESSAGE_H -#define SHAPER_MESSAGE_H - namespace Shared { /** @@ -46,9 +46,22 @@ public: delivered, error }; + static const State StateHighest = State::error; static const State StateLowest = State::pending; + struct Change //change functor, stores in idModified if ID has been modified during change + { + Change(const QMap& _data); + void operator() (Message& msg); + void operator() (Message* msg); + bool hasIdBeenModified() const; + + private: + const QMap& data; + bool idModified; + }; + Message(Type p_type); Message(); @@ -72,6 +85,7 @@ public: void setErrorText(const QString& err); bool change(const QMap& data); void setStanzaId(const QString& sid); + void setAttachPath(const QString& path); QString getFrom() const; QString getFromJid() const; @@ -100,6 +114,7 @@ public: QDateTime getLastModified() const; QString getOriginalBody() const; QString getStanzaId() const; + QString getAttachPath() const; void serialize(QDataStream& data) const; void deserialize(QDataStream& data); @@ -123,6 +138,7 @@ private: QString originalMessage; QDateTime lastModified; QString stanzaId; + QString attachPath; }; } diff --git a/shared/messageinfo.cpp b/shared/messageinfo.cpp new file mode 100644 index 0000000..7502a6e --- /dev/null +++ b/shared/messageinfo.cpp @@ -0,0 +1,45 @@ +/* + * Squawk messenger. + * Copyright (C) 2019 Yury Gubich + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "messageinfo.h" + +using namespace Shared; + +Shared::MessageInfo::MessageInfo(): + account(), + jid(), + messageId() {} + +Shared::MessageInfo::MessageInfo(const QString& acc, const QString& j, const QString& id): + account(acc), + jid(j), + messageId(id) {} + +Shared::MessageInfo::MessageInfo(const Shared::MessageInfo& other): + account(other.account), + jid(other.jid), + messageId(other.messageId) {} + +Shared::MessageInfo & Shared::MessageInfo::operator=(const Shared::MessageInfo& other) +{ + account = other.account; + jid = other.jid; + messageId = other.messageId; + + return *this; +} diff --git a/shared/messageinfo.h b/shared/messageinfo.h new file mode 100644 index 0000000..942d88c --- /dev/null +++ b/shared/messageinfo.h @@ -0,0 +1,43 @@ +/* + * Squawk messenger. + * Copyright (C) 2019 Yury Gubich + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef SHARED_MESSAGEINFO_H +#define SHARED_MESSAGEINFO_H + +#include + +namespace Shared { + +/** + * @todo write docs + */ +struct MessageInfo { + MessageInfo(); + MessageInfo(const QString& acc, const QString& j, const QString& id); + MessageInfo(const MessageInfo& other); + + QString account; + QString jid; + QString messageId; + + MessageInfo& operator=(const MessageInfo& other); +}; + +} + +#endif // SHARED_MESSAGEINFO_H diff --git a/order.h b/shared/order.h similarity index 100% rename from order.h rename to shared/order.h diff --git a/shared/qxmppfactories.cpp b/shared/qxmppfactories.cpp new file mode 100644 index 0000000..dc53388 --- /dev/null +++ b/shared/qxmppfactories.cpp @@ -0,0 +1,75 @@ +/* + * Created by victoria on 2021-05-12. + */ + +#include "qxmppfactories.h" + +#include + +bool QXmpp::Factories::elementMatches(const QXmppElement &element, + const QString &tagName, + const QString &xmlns) { + if (element.tagName() != tagName) { + qWarning() << "tag name: expected = " << tagName + << ", got = " << element.tagName(); + return false; + } + + if (!xmlns.isEmpty() && element.attribute("xmlns") != xmlns) { + qWarning() << "xmlns: expected = " << xmlns + << ", got = " << element.attribute("xmlns"); + return false; + } + + return true; +} + +QXmppElement QXmpp::Factories::createElement(const QString &tagName, + const QString &xmlns) { + QXmppElement el{}; + el.setTagName(tagName); + + if (!xmlns.isEmpty()) + el.setAttribute("xmlns", xmlns); + + return el; +} + +QXmppElement QXmpp::Factories::createValue(const QString &value) { + auto el = createElement("value"); + el.setValue(value); + + return el; +} + +QXmppElement QXmpp::Factories::createField(const QString &key, + const QString &value, bool hidden) { + auto field = createElement("field"); + field.setAttribute("var", key); + if (hidden) + field.setAttribute("type", "hidden"); + field.appendChild(createValue(value)); + + return field; +} + +QXmppElement +QXmpp::Factories::createOpenPublishOptions(const QString &maxItems) { + auto formType = createField( + "FORM_TYPE", "http://jabber.org/protocol/pubsub#publish-options", true); + auto accessModel = createField("pubsub#access_model", "open"); + + auto x = createElement("x", "jabber:x:data"); + x.setAttribute("type", "submit"); + + x.appendChild(formType); + x.appendChild(accessModel); + + if (!maxItems.isEmpty()) + x.appendChild(createField("pubsub#max_items", maxItems)); + + auto publishOptions = createElement("publish-options"); + publishOptions.appendChild(x); + + return publishOptions; +} \ No newline at end of file diff --git a/shared/qxmppfactories.h b/shared/qxmppfactories.h new file mode 100644 index 0000000..d7d78e6 --- /dev/null +++ b/shared/qxmppfactories.h @@ -0,0 +1,25 @@ +/* + * Created by victoria on 2021-05-12. + */ + +#pragma once + +#include + +namespace QXmpp::Factories { + +bool elementMatches(const QXmppElement &element, const QString &tagName, + const QString &xmlns = QStringLiteral("")); + +QXmppElement createElement(const QString &tagName, + const QString &xmlns = QStringLiteral("")); + +QXmppElement createValue(const QString &value); + +QXmppElement createField(const QString &key, const QString &value, + bool hidden = false); + +QXmppElement +createOpenPublishOptions(const QString &maxItems = QStringLiteral("")); + +} // namespace QXmpp::Factories diff --git a/shared.h b/shared/shared.h similarity index 83% rename from shared.h rename to shared/shared.h index 83bcd76..1e86c5a 100644 --- a/shared.h +++ b/shared/shared.h @@ -19,11 +19,12 @@ #ifndef SHARED_H #define SHARED_H -#include "shared/enums.h" -#include "shared/utils.h" -#include "shared/icons.h" -#include "shared/message.h" -#include "shared/vcard.h" -#include "shared/global.h" +#include "enums.h" +#include "global.h" +#include "icons.h" +#include "message.h" +#include "messageinfo.h" +#include "utils.h" +#include "vcard.h" #endif // SHARED_H diff --git a/shared/utils.h b/shared/utils.h index e9e3d29..a8a17d5 100644 --- a/shared/utils.h +++ b/shared/utils.h @@ -20,11 +20,14 @@ #define SHARED_UTILS_H #include +#include #include #include +//#include "KIO/OpenFileManagerWindowJob" + #include -#include +#include namespace Shared { diff --git a/translations/CMakeLists.txt b/translations/CMakeLists.txt new file mode 100644 index 0000000..c484000 --- /dev/null +++ b/translations/CMakeLists.txt @@ -0,0 +1,8 @@ +find_package(Qt5LinguistTools) + +set(TS_FILES squawk.ru.ts) +qt5_add_translation(QM_FILES ${TS_FILES}) +add_custom_target(translations ALL DEPENDS ${QM_FILES}) +install(FILES ${QM_FILES} DESTINATION ${CMAKE_INSTALL_DATADIR}/squawk/l10n) + +add_dependencies(${CMAKE_PROJECT_NAME} translations) \ No newline at end of file diff --git a/ui/CMakeLists.txt b/ui/CMakeLists.txt index 52913a8..222f8d7 100644 --- a/ui/CMakeLists.txt +++ b/ui/CMakeLists.txt @@ -1,45 +1,6 @@ -cmake_minimum_required(VERSION 3.0) -project(squawkUI) - -# Instruct CMake to run moc automatically when needed. -set(CMAKE_AUTOMOC ON) -# Instruct CMake to create code from Qt designer ui files -set(CMAKE_AUTOUIC ON) - -# Find the QtWidgets library -find_package(Qt5Widgets CONFIG REQUIRED) -find_package(Qt5DBus CONFIG REQUIRED) +target_sources(squawk PRIVATE squawk.cpp squawk.h squawk.ui) +add_subdirectory(models) +add_subdirectory(utils) add_subdirectory(widgets) - -set(squawkUI_SRC - squawk.cpp - models/accounts.cpp - models/roster.cpp - models/item.cpp - models/account.cpp - models/contact.cpp - models/presence.cpp - models/group.cpp - models/room.cpp - models/abstractparticipant.cpp - models/participant.cpp - models/reference.cpp - utils/messageline.cpp - utils//message.cpp - utils/resizer.cpp - utils/image.cpp - utils/flowlayout.cpp - utils/badge.cpp - utils/progress.cpp - utils/comboboxdelegate.cpp - utils/dropshadoweffect.cpp -) - -# Tell CMake to create the helloworld executable -add_library(squawkUI ${squawkUI_SRC}) - -# Use the Widgets module from Qt 5. -target_link_libraries(squawkUI squawkWidgets) -target_link_libraries(squawkUI Qt5::Widgets) -target_link_libraries(squawkUI Qt5::DBus) +add_subdirectory(omemo) \ No newline at end of file diff --git a/ui/models/CMakeLists.txt b/ui/models/CMakeLists.txt new file mode 100644 index 0000000..98ef1c3 --- /dev/null +++ b/ui/models/CMakeLists.txt @@ -0,0 +1,28 @@ +target_sources(squawk PRIVATE + abstractparticipant.cpp + abstractparticipant.h + account.cpp + account.h + accounts.cpp + accounts.h + contact.cpp + contact.h + element.cpp + element.h + group.cpp + group.h + item.cpp + item.h + messagefeed.cpp + messagefeed.h + participant.cpp + participant.h + presence.cpp + presence.h + reference.cpp + reference.h + room.cpp + room.h + roster.cpp + roster.h + ) \ No newline at end of file diff --git a/ui/models/account.cpp b/ui/models/account.cpp index 00dd6b2..f8d0c37 100644 --- a/ui/models/account.cpp +++ b/ui/models/account.cpp @@ -231,7 +231,7 @@ void Models::Account::toOfflineState() Item::toOfflineState(); } -QString Models::Account::getAvatarPath() +QString Models::Account::getAvatarPath() const { return avatarPath; } diff --git a/ui/models/account.h b/ui/models/account.h index 2563382..686d4da 100644 --- a/ui/models/account.h +++ b/ui/models/account.h @@ -57,7 +57,7 @@ namespace Models { QString getError() const; void setAvatarPath(const QString& path); - QString getAvatarPath(); + QString getAvatarPath() const; void setAvailability(Shared::Availability p_avail); void setAvailability(unsigned int p_avail); diff --git a/ui/models/contact.cpp b/ui/models/contact.cpp index 57744d8..d54fccf 100644 --- a/ui/models/contact.cpp +++ b/ui/models/contact.cpp @@ -17,55 +17,26 @@ */ #include "contact.h" -#include "account.h" #include Models::Contact::Contact(const Account* acc, const QString& p_jid ,const QMap &data, Item *parentItem): - Item(Item::contact, data, parentItem), - jid(p_jid), + Element(Item::contact, acc, p_jid, data, parentItem), availability(Shared::Availability::offline), state(Shared::SubscriptionState::none), - avatarState(Shared::Avatar::empty), presences(), - messages(), - childMessages(0), - status(), - avatarPath(), - account(acc) + status() { QMap::const_iterator itr = data.find("state"); if (itr != data.end()) { setState(itr.value().toUInt()); } - - itr = data.find("avatarState"); - if (itr != data.end()) { - setAvatarState(itr.value().toUInt()); - } - itr = data.find("avatarPath"); - if (itr != data.end()) { - setAvatarPath(itr.value().toString()); - } } Models::Contact::~Contact() { } -QString Models::Contact::getJid() const -{ - return jid; -} - -void Models::Contact::setJid(const QString p_jid) -{ - if (jid != p_jid) { - jid = p_jid; - changed(1); - } -} - void Models::Contact::setAvailability(unsigned int p_state) { setAvailability(Shared::Global::fromInt(p_state)); @@ -144,16 +115,12 @@ void Models::Contact::update(const QString& field, const QVariant& value) { if (field == "name") { setName(value.toString()); - } else if (field == "jid") { - setJid(value.toString()); } else if (field == "availability") { setAvailability(value.toUInt()); } else if (field == "state") { setState(value.toUInt()); - } else if (field == "avatarState") { - setAvatarState(value.toUInt()); - } else if (field == "avatarPath") { - setAvatarPath(value.toString()); + } else { + Element::update(field, value); } } @@ -192,11 +159,9 @@ void Models::Contact::refresh() { QDateTime lastActivity; Presence* presence = 0; - unsigned int count = 0; for (QMap::iterator itr = presences.begin(), end = presences.end(); itr != end; ++itr) { Presence* pr = itr.value(); QDateTime la = pr->getLastActivity(); - count += pr->getMessagesCount(); if (la > lastActivity) { lastActivity = la; @@ -211,11 +176,6 @@ void Models::Contact::refresh() setAvailability(Shared::Availability::offline); setStatus(""); } - - if (childMessages != count) { - childMessages = count; - changed(4); - } } void Models::Contact::_removeChild(int index) @@ -257,81 +217,6 @@ QIcon Models::Contact::getStatusIcon(bool big) const } } -void Models::Contact::addMessage(const Shared::Message& data) -{ - const QString& res = data.getPenPalResource(); - if (res.size() > 0) { - QMap::iterator itr = presences.find(res); - if (itr == presences.end()) { - // this is actually the place when I can spot someone's invisible presence, and there is nothing criminal in it, cuz the sender sent us a message - // therefore he have revealed himself - // the only issue is to find out when the sender is gone offline - Presence* pr = new Presence({}); - pr->setName(res); - pr->setAvailability(Shared::Availability::invisible); - pr->setLastActivity(QDateTime::currentDateTimeUtc()); - presences.insert(res, pr); - appendChild(pr); - pr->addMessage(data); - return; - } - itr.value()->addMessage(data); - } else { - messages.emplace_back(data); - changed(4); - } -} - -void Models::Contact::changeMessage(const QString& id, const QMap& data) -{ - - bool found = false; - for (Shared::Message& msg : messages) { - if (msg.getId() == id) { - msg.change(data); - found = true; - break; - } - } - if (!found) { - for (Presence* pr : presences) { - found = pr->changeMessage(id, data); - if (found) { - break; - } - } - } -} - -unsigned int Models::Contact::getMessagesCount() const -{ - return messages.size() + childMessages; -} - -void Models::Contact::dropMessages() -{ - if (messages.size() > 0) { - messages.clear(); - changed(4); - } - - for (QMap::iterator itr = presences.begin(), end = presences.end(); itr != end; ++itr) { - itr.value()->dropMessages(); - } -} - -void Models::Contact::getMessages(Models::Contact::Messages& container) const -{ - for (Messages::const_iterator itr = messages.begin(), end = messages.end(); itr != end; ++itr) { - const Shared::Message& msg = *itr; - container.push_back(msg); - } - - for (QMap::const_iterator itr = presences.begin(), end = presences.end(); itr != end; ++itr) { - itr.value()->getMessages(container); - } -} - void Models::Contact::toOfflineState() { std::deque::size_type size = childItems.size(); @@ -355,75 +240,3 @@ QString Models::Contact::getDisplayedName() const return getContactName(); } -bool Models::Contact::columnInvolvedInDisplay(int col) -{ - return Item::columnInvolvedInDisplay(col) && col == 1; -} - -Models::Contact * Models::Contact::copy() const -{ - Contact* cnt = new Contact(*this); - return cnt; -} - -Models::Contact::Contact(const Models::Contact& other): - Item(other), - jid(other.jid), - availability(other.availability), - state(other.state), - presences(), - messages(other.messages), - childMessages(0), - account(other.account) -{ - for (const Presence* pres : other.presences) { - Presence* pCopy = new Presence(*pres); - presences.insert(pCopy->getName(), pCopy); - Item::appendChild(pCopy); - connect(pCopy, &Item::childChanged, this, &Contact::refresh); - } - - refresh(); -} - -QString Models::Contact::getAvatarPath() const -{ - return avatarPath; -} - -Shared::Avatar Models::Contact::getAvatarState() const -{ - return avatarState; -} - -void Models::Contact::setAvatarPath(const QString& path) -{ - if (path != avatarPath) { - avatarPath = path; - changed(7); - } -} - -void Models::Contact::setAvatarState(Shared::Avatar p_state) -{ - if (avatarState != p_state) { - avatarState = p_state; - changed(6); - } -} - -void Models::Contact::setAvatarState(unsigned int p_state) -{ - if (p_state <= static_cast(Shared::Avatar::valid)) { - Shared::Avatar state = static_cast(p_state); - setAvatarState(state); - } else { - qDebug() << "An attempt to set invalid avatar state" << p_state << "to the contact" << jid << ", skipping"; - } -} - -const Models::Account * Models::Contact::getParentAccount() const -{ - return account; -} - diff --git a/ui/models/contact.h b/ui/models/contact.h index c8c99b5..7e76f5b 100644 --- a/ui/models/contact.h +++ b/ui/models/contact.h @@ -19,7 +19,7 @@ #ifndef MODELS_CONTACT_H #define MODELS_CONTACT_H -#include "item.h" +#include "element.h" #include "presence.h" #include "shared/enums.h" #include "shared/message.h" @@ -31,49 +31,34 @@ #include namespace Models { -class Account; -class Contact : public Item +class Contact : public Element { Q_OBJECT public: - typedef std::deque Messages; Contact(const Account* acc, const QString& p_jid, const QMap &data, Item *parentItem = 0); - Contact(const Contact& other); ~Contact(); - QString getJid() const; Shared::Availability getAvailability() const; Shared::SubscriptionState getState() const; - Shared::Avatar getAvatarState() const; - QString getAvatarPath() const; + QIcon getStatusIcon(bool big = false) const; int columnCount() const override; QVariant data(int column) const override; - void update(const QString& field, const QVariant& value); + void update(const QString& field, const QVariant& value) override; void addPresence(const QString& name, const QMap& data); void removePresence(const QString& name); QString getContactName() const; QString getStatus() const; - - void addMessage(const Shared::Message& data); - void changeMessage(const QString& id, const QMap& data); - unsigned int getMessagesCount() const; - void dropMessages(); - void getMessages(Messages& container) const; QString getDisplayedName() const override; - Contact* copy() const; - protected: void _removeChild(int index) override; void _appendChild(Models::Item * child) override; - bool columnInvolvedInDisplay(int col) override; - const Account* getParentAccount() const override; protected slots: void refresh(); @@ -84,23 +69,13 @@ protected: void setAvailability(unsigned int p_state); void setState(Shared::SubscriptionState p_state); void setState(unsigned int p_state); - void setAvatarState(Shared::Avatar p_state); - void setAvatarState(unsigned int p_state); - void setAvatarPath(const QString& path); - void setJid(const QString p_jid); void setStatus(const QString& p_state); private: - QString jid; Shared::Availability availability; Shared::SubscriptionState state; - Shared::Avatar avatarState; QMap presences; - Messages messages; - unsigned int childMessages; QString status; - QString avatarPath; - const Account* account; }; } diff --git a/ui/models/element.cpp b/ui/models/element.cpp new file mode 100644 index 0000000..4e741a4 --- /dev/null +++ b/ui/models/element.cpp @@ -0,0 +1,184 @@ +/* + * Squawk messenger. + * Copyright (C) 2019 Yury Gubich + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "element.h" +#include "account.h" + +#include + +Models::Element::Element(Type p_type, const Models::Account* acc, const QString& p_jid, const QMap& data, Models::Item* parentItem): + Item(p_type, data, parentItem), + jid(p_jid), + avatarPath(), + avatarState(Shared::Avatar::empty), + account(acc), + feed(new MessageFeed(this)) +{ + connect(feed, &MessageFeed::requestArchive, this, &Element::requestArchive); + connect(feed, &MessageFeed::fileDownloadRequest, this, &Element::fileDownloadRequest); + connect(feed, &MessageFeed::unreadMessagesCountChanged, this, &Element::onFeedUnreadMessagesCountChanged); + connect(feed, &MessageFeed::unnoticedMessage, this, &Element::onFeedUnnoticedMessage); + connect(feed, &MessageFeed::localPathInvalid, this, &Element::localPathInvalid); + + QMap::const_iterator itr = data.find("avatarState"); + if (itr != data.end()) { + setAvatarState(itr.value().toUInt()); + } + itr = data.find("avatarPath"); + if (itr != data.end()) { + setAvatarPath(itr.value().toString()); + } +} + +Models::Element::~Element() +{ + delete feed; +} + + +QString Models::Element::getJid() const +{ + return jid; +} + +void Models::Element::setJid(const QString& p_jid) +{ + if (jid != p_jid) { + jid = p_jid; + changed(1); + } +} + +void Models::Element::update(const QString& field, const QVariant& value) +{ + if (field == "jid") { + setJid(value.toString()); + } else if (field == "avatarState") { + setAvatarState(value.toUInt()); + } else if (field == "avatarPath") { + setAvatarPath(value.toString()); + } +} + +QString Models::Element::getAvatarPath() const +{ + return avatarPath; +} + +Shared::Avatar Models::Element::getAvatarState() const +{ + return avatarState; +} + +void Models::Element::setAvatarPath(const QString& path) +{ + if (path != avatarPath) { + avatarPath = path; + if (type == contact) { + changed(7); + } else if (type == room) { + changed(8); + } + } +} + +void Models::Element::setAvatarState(Shared::Avatar p_state) +{ + if (avatarState != p_state) { + avatarState = p_state; + if (type == contact) { + changed(6); + } else if (type == room) { + changed(7); + } + } +} + +void Models::Element::setAvatarState(unsigned int p_state) +{ + if (p_state <= static_cast(Shared::Avatar::valid)) { + Shared::Avatar state = static_cast(p_state); + setAvatarState(state); + } else { + qDebug() << "An attempt to set invalid avatar state" << p_state << "to the element" << jid << ", skipping"; + } +} + +bool Models::Element::columnInvolvedInDisplay(int col) +{ + return Item::columnInvolvedInDisplay(col) && col == 1; +} + +const Models::Account * Models::Element::getParentAccount() const +{ + return account; +} + +unsigned int Models::Element::getMessagesCount() const +{ + return feed->unreadMessagesCount(); +} + +void Models::Element::addMessage(const Shared::Message& data) +{ + feed->addMessage(data); +} + +void Models::Element::changeMessage(const QString& id, const QMap& data) +{ + feed->changeMessage(id, data); +} + +void Models::Element::responseArchive(const std::list list, bool last) +{ + feed->responseArchive(list, last); +} + +bool Models::Element::isRoom() const +{ + return type != contact; +} + +void Models::Element::fileProgress(const QString& messageId, qreal value, bool up) +{ + feed->fileProgress(messageId, value, up); +} + +void Models::Element::fileComplete(const QString& messageId, bool up) +{ + feed->fileComplete(messageId, up); +} + +void Models::Element::fileError(const QString& messageId, const QString& error, bool up) +{ + feed->fileError(messageId, error, up); +} + +void Models::Element::onFeedUnreadMessagesCountChanged() +{ + if (type == contact) { + changed(4); + } else if (type == room) { + changed(5); + } +} + +void Models::Element::onFeedUnnoticedMessage(const Shared::Message& msg) +{ + emit unnoticedMessage(getAccountName(), msg); +} diff --git a/ui/models/element.h b/ui/models/element.h new file mode 100644 index 0000000..af44791 --- /dev/null +++ b/ui/models/element.h @@ -0,0 +1,81 @@ +/* + * Squawk messenger. + * Copyright (C) 2019 Yury Gubich + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef ELEMENT_H +#define ELEMENT_H + +#include "item.h" +#include "messagefeed.h" + +namespace Models { + +class Element : public Item +{ + Q_OBJECT +protected: + Element(Type p_type, const Account* acc, const QString& p_jid, const QMap &data, Item *parentItem = 0); + ~Element(); + +public: + QString getJid() const; + Shared::Avatar getAvatarState() const; + QString getAvatarPath() const; + + virtual void update(const QString& field, const QVariant& value); + + void addMessage(const Shared::Message& data); + void changeMessage(const QString& id, const QMap& data); + unsigned int getMessagesCount() const; + void responseArchive(const std::list list, bool last); + bool isRoom() const; + void fileProgress(const QString& messageId, qreal value, bool up); + void fileError(const QString& messageId, const QString& error, bool up); + void fileComplete(const QString& messageId, bool up); + +signals: + void requestArchive(const QString& before); + void fileDownloadRequest(const QString& url); + void unnoticedMessage(const QString& account, const Shared::Message& msg); + void localPathInvalid(const QString& path); + +protected: + void setJid(const QString& p_jid); + void setAvatarState(Shared::Avatar p_state); + void setAvatarState(unsigned int p_state); + void setAvatarPath(const QString& path); + bool columnInvolvedInDisplay(int col) override; + const Account* getParentAccount() const override; + +protected slots: + void onFeedUnreadMessagesCountChanged(); + void onFeedUnnoticedMessage(const Shared::Message& msg); + +protected: + QString jid; + QString avatarPath; + Shared::Avatar avatarState; + + const Account* account; + +public: + MessageFeed* feed; +}; + +} + +#endif // ELEMENT_H diff --git a/ui/models/item.cpp b/ui/models/item.cpp index e006ad0..4a88dd2 100644 --- a/ui/models/item.cpp +++ b/ui/models/item.cpp @@ -283,6 +283,15 @@ Shared::ConnectionState Models::Item::getAccountConnectionState() const return acc->getState(); } +QString Models::Item::getAccountAvatarPath() const +{ + const Account* acc = getParentAccount(); + if (acc == nullptr) { + return ""; + } + return acc->getAvatarPath(); +} + QString Models::Item::getDisplayedName() const { return name; diff --git a/ui/models/item.h b/ui/models/item.h index 4f3e29a..4661479 100644 --- a/ui/models/item.h +++ b/ui/models/item.h @@ -80,6 +80,7 @@ class Item : public QObject{ QString getAccountName() const; QString getAccountJid() const; QString getAccountResource() const; + QString getAccountAvatarPath() const; Shared::ConnectionState getAccountConnectionState() const; Shared::Availability getAccountAvailability() const; diff --git a/ui/models/messagefeed.cpp b/ui/models/messagefeed.cpp new file mode 100644 index 0000000..d5fb3bc --- /dev/null +++ b/ui/models/messagefeed.cpp @@ -0,0 +1,579 @@ +/* + * Squawk messenger. + * Copyright (C) 2019 Yury Gubich + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "messagefeed.h" +#include "element.h" +#include "room.h" + +#include + +const QHash Models::MessageFeed::roles = { + {Text, "text"}, + {Sender, "sender"}, + {Date, "date"}, + {DeliveryState, "deliveryState"}, + {Correction, "correction"}, + {SentByMe,"sentByMe"}, + {Avatar, "avatar"}, + {Attach, "attach"}, + {Id, "id"}, + {Error, "error"}, + {Bulk, "bulk"} +}; + +Models::MessageFeed::MessageFeed(const Element* ri, QObject* parent): + QAbstractListModel(parent), + storage(), + indexById(storage.get()), + indexByTime(storage.get