From b848a10e17b57de3b107caf2889fcea0c7976060 Mon Sep 17 00:00:00 2001 From: Markus Bader Date: Wed, 22 Jan 2025 12:14:27 +0100 Subject: [PATCH 01/12] feat: CardDAV contact support Fixes #11 --- .github/workflows/gonnect.yml | 2 +- CMakeLists.txt | 2 + README.md | 2 +- patches/qtwebdav-cmake-lowercase.diff | 30 ++ patches/vcard-disable-testing.diff | 10 + resources/flatpak/de.gonicus.gonnect.yml | 18 ++ .../flatpak/patches/qtwebdav-cmake.patch | 69 +++++ resources/flatpak/patches/vcard-cmake.patch | 42 +++ resources/templates/sample.conf | 26 ++ sample.conf | 29 ++ src/CMakeLists.txt | 38 ++- src/Main.qml | 8 +- src/contacts/AddressBook.cpp | 8 +- src/contacts/AddressBook.h | 6 +- src/contacts/AddressBookManager.cpp | 92 +++++- src/contacts/AddressBookManager.h | 4 + src/contacts/AvatarManager.cpp | 53 +++- src/contacts/AvatarManager.h | 3 + src/contacts/CardDAVAddressBookFeeder.cpp | 280 ++++++++++++++++++ src/contacts/CardDAVAddressBookFeeder.h | 43 +++ src/contacts/Contact.cpp | 2 + src/contacts/Contact.h | 1 + src/media/AudioPort.cpp | 5 +- src/ui/ViewHelper.cpp | 10 + src/ui/ViewHelper.h | 6 + ...ntialsDialog.qml => CredentialsDialog.qml} | 10 +- 26 files changed, 778 insertions(+), 21 deletions(-) create mode 100644 patches/qtwebdav-cmake-lowercase.diff create mode 100644 patches/vcard-disable-testing.diff create mode 100644 resources/flatpak/patches/qtwebdav-cmake.patch create mode 100644 resources/flatpak/patches/vcard-cmake.patch create mode 100644 src/contacts/CardDAVAddressBookFeeder.cpp create mode 100644 src/contacts/CardDAVAddressBookFeeder.h rename src/ui/components/dialogs/{AccountCredentialsDialog.qml => CredentialsDialog.qml} (83%) diff --git a/.github/workflows/gonnect.yml b/.github/workflows/gonnect.yml index 8281064..98d0119 100644 --- a/.github/workflows/gonnect.yml +++ b/.github/workflows/gonnect.yml @@ -50,7 +50,7 @@ jobs: qt-version: ${{ env.QT_VERSION }} - name: Configure CMake - run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DBUILD_TESTING=ON -DCMAKE_PREFIX_PATH="${{github.workspace}}/pjproject;${{github.workspace}}/qca/lib/cmake" + run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DBUILD_TESTING=ON -DBUILD_DEPENDENCIES=ON -DCMAKE_PREFIX_PATH="${{github.workspace}}/pjproject;${{github.workspace}}/qca/lib/cmake" - name: Build # Build your program with the given configuration diff --git a/CMakeLists.txt b/CMakeLists.txt index 2a06a00..a488d29 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,6 +8,8 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_POSITION_INDEPENDENT_CODE TRUE) set(CMAKE_INCLUDE_CURRENT_DIR ON) +option(BUILD_DEPENDENCIES "Build dependencies" OFF) + if(NOT LINUX) message(FATAL_ERROR "GOnnect only works for Linux/Flatpack targets") endif() diff --git a/README.md b/README.md index abd657c..b367b70 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Here's a short feature list: * Call forwarding * Conference calls with three parties - * LDAP address sources + * LDAP/CardDAV address sources * Busy state for supported sources * Configurable identities for outgoing calls * Configurable busy on active call diff --git a/patches/qtwebdav-cmake-lowercase.diff b/patches/qtwebdav-cmake-lowercase.diff new file mode 100644 index 0000000..a8f2bb5 --- /dev/null +++ b/patches/qtwebdav-cmake-lowercase.diff @@ -0,0 +1,30 @@ +diff --git a/CMakeLists.txt b/CMakeLists.txt +index 6883604..c410318 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -5,15 +5,15 @@ find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Network Xml REQUIRED) + set(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}) + set(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}) + add_library(QtWebDAV SHARED +- QNaturalSort.cpp +- QNaturalSort.h +- QWebDAV.cpp +- QWebDAV.h +- QWebDAV_global.h +- QWebDAVDirParser.cpp +- QWebDAVDirParser.h +- QWebDAVItem.cpp +- QWebDAVItem.h ++ qnaturalsort.cpp ++ qnaturalsort.h ++ qwebdav.cpp ++ qwebdav.h ++ qwebdav_global.h ++ qwebdavdirparser.cpp ++ qwebdavdirparser.h ++ qwebdavitem.cpp ++ qwebdavitem.h + ) + + target_link_libraries(QtWebDAV PUBLIC + diff --git a/patches/vcard-disable-testing.diff b/patches/vcard-disable-testing.diff new file mode 100644 index 0000000..eff6e81 --- /dev/null +++ b/patches/vcard-disable-testing.diff @@ -0,0 +1,10 @@ +diff --git a/CMakeLists.txt b/CMakeLists.txt +index b8d0506..0f14d34 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -6,5 +6,3 @@ set(CMAKE_CXX_STANDARD 14) + include_directories(include) + + add_subdirectory(src) +-ENABLE_TESTING() +-add_subdirectory(test) diff --git a/resources/flatpak/de.gonicus.gonnect.yml b/resources/flatpak/de.gonicus.gonnect.yml index 1942a77..aa61803 100644 --- a/resources/flatpak/de.gonicus.gonnect.yml +++ b/resources/flatpak/de.gonicus.gonnect.yml @@ -67,6 +67,24 @@ modules: url: "https://www.openldap.org/software/download/OpenLDAP/openldap-release/openldap-2.6.9.tgz" sha256: 2cb7dc73e9c8340dff0d99357fbaa578abf30cc6619f0521972c555681e6b2ff +- name: qtwebdav + buildsystem: cmake-ninja + sources: + - type: git + url: https://github.com/PikachuHy/QtWebDAV.git + commit: 4739a0f09cd005b9584f740637882be41ec0f062 + - type: patch + path: patches/qtwebdav-cmake.patch + +- name: vcard + buildsystem: cmake-ninja + sources: + - type: git + url: https://github.com/ivanenko/vCard.git + commit: 733afa9571a728548106c5131c48895bd932e881 + - type: patch + path: patches/vcard-cmake.patch + - shared-modules/libusb/libusb.json - name: hidapi diff --git a/resources/flatpak/patches/qtwebdav-cmake.patch b/resources/flatpak/patches/qtwebdav-cmake.patch new file mode 100644 index 0000000..4371093 --- /dev/null +++ b/resources/flatpak/patches/qtwebdav-cmake.patch @@ -0,0 +1,69 @@ +diff --git a/CMakeLists.txt b/CMakeLists.txt +index 6883604..7be4550 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -2,29 +2,50 @@ cmake_minimum_required(VERSION 3.19) + project(QtWebDAV) + find_package(QT NAMES Qt6 Qt5 COMPONENTS Network Xml REQUIRED) + find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Network Xml REQUIRED) +-set(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}) +-set(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}) ++ + add_library(QtWebDAV SHARED +- QNaturalSort.cpp +- QNaturalSort.h +- QWebDAV.cpp +- QWebDAV.h +- QWebDAV_global.h +- QWebDAVDirParser.cpp +- QWebDAVDirParser.h +- QWebDAVItem.cpp +- QWebDAVItem.h ++ qnaturalsort.cpp ++ qnaturalsort.h ++ qwebdav.cpp ++ qwebdav.h ++ qwebdav_global.h ++ qwebdavdirparser.cpp ++ qwebdavdirparser.h ++ qwebdavitem.cpp ++ qwebdavitem.h + ) + + target_link_libraries(QtWebDAV PUBLIC + Qt${QT_VERSION_MAJOR}::Network + Qt${QT_VERSION_MAJOR}::Xml + ) +-target_include_directories(QtWebDAV PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) ++target_include_directories(QtWebDAV PUBLIC ++ $ ++ $ ++) + target_compile_definitions(QtWebDAV PRIVATE -DQWEBDAV_LIBRARY) + target_compile_definitions(QtWebDAV PRIVATE DEBUG_WEBDAV) + set_target_properties(QtWebDAV PROPERTIES AUTOMOC ON) ++ ++file(GLOB_RECURSE QTWEBDAV_INCLUDE_FILES "${CMAKE_CURRENT_SOURCE_DIR}/*.h") ++set_target_properties(${PROJECT_NAME} ++ PROPERTIES PUBLIC_HEADER "${QTWEBDAV_INCLUDE_FILES}" ++) ++ + option(BUILD_EXAMPLE "Build with example") + if (BUILD_EXAMPLE) + add_subdirectory(example) +-endif() +\ No newline at end of file ++endif() ++ ++install(TARGETS ${CMAKE_PROJECT_NAME} ++ EXPORT targets ++ LIBRARY ++ DESTINATION ${CMAKE_INSTALL_LIBDIR} ++ PUBLIC_HEADER ++ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} ++) ++ ++install(EXPORT targets ++ FILE ${CMAKE_PROJECT_NAME}Config.cmake ++ DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${CMAKE_PROJECT_NAME} ++) diff --git a/resources/flatpak/patches/vcard-cmake.patch b/resources/flatpak/patches/vcard-cmake.patch new file mode 100644 index 0000000..340df19 --- /dev/null +++ b/resources/flatpak/patches/vcard-cmake.patch @@ -0,0 +1,42 @@ +diff --git a/CMakeLists.txt b/CMakeLists.txt +index b8d0506..0f14d34 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -6,5 +6,3 @@ set(CMAKE_CXX_STANDARD 14) + include_directories(include) + + add_subdirectory(src) +-ENABLE_TESTING() +-add_subdirectory(test) +diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt +index f24ebe4..434b9ec 100644 +--- a/src/CMakeLists.txt ++++ b/src/CMakeLists.txt +@@ -8,12 +8,24 @@ utils.cpp ) + + target_include_directories(${CMAKE_PROJECT_NAME} + PUBLIC +- $ ++ $ + $ + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/src + ) + ++file(GLOB_RECURSE VCARD_INCLUDE_FILES "${PROJECT_SOURCE_DIR}/include/*.h") ++set_target_properties(${PROJECT_NAME} ++ PROPERTIES PUBLIC_HEADER "${VCARD_INCLUDE_FILES}" ++) ++ ++include(GNUInstallDirs) + install(TARGETS ${CMAKE_PROJECT_NAME} ++ EXPORT targets + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} +- PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) +\ No newline at end of file ++ PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) ++ ++install(EXPORT targets ++ FILE ${CMAKE_PROJECT_NAME}Config.cmake ++ DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${CMAKE_PROJECT_NAME} ++) diff --git a/resources/templates/sample.conf b/resources/templates/sample.conf index bdcd354..a76b1e2 100644 --- a/resources/templates/sample.conf +++ b/resources/templates/sample.conf @@ -463,3 +463,29 @@ port=5061 ## (optional) #sipStatusSubscriptableAttributes="telephoneNumber" +## A CardDAV block defines a CardDAV source for the address book. +## The password for the authentication will be requested in a UI dialog on next start of the app. It will then be saved encrypted for further usage in +## the config file named "keychain". Delete the appropriate part there to enforce a new password request. +## The contacts will be loaded asynchronously on app start. Depending on quantity and server speed, it might take a few minutes until all contacts +## are loaded. +## Example (all fields are mandatory unless specified as optional): + +## Each block (= CardDAV source) must have the name "carddav", followed by one or more digits. +# [carddav0] + +## The host is the main host address of the remote server, without any port, protocol or path data. +# host=cloud.mycompany.com + +## The path relative to the host were to find the resource. +# path=/remote.php/dav/addressbooks/users/john/contacts/ + +## The user name required for the authentication. +# user=john + +## The server port. +## (optional, defaults: 443 is used if useSSL is true, 80 if false) +# port=443 + +## Whether to use SSL (https) or not (http). +## (optional, default: true) +# useSSL=true diff --git a/sample.conf b/sample.conf index 60847fa..c5b4fee 100644 --- a/sample.conf +++ b/sample.conf @@ -458,3 +458,32 @@ ## (optional) #sipStatusSubscriptableAttributes="telephoneNumber" + +## A CardDAV block defines a CardDAV source for the address book. +## The password for the authentication will be requested in a UI dialog on next start of the app. It will then be saved encrypted for further usage in +## the config file named "keychain". Delete the appropriate part there to enforce a new password request. +## The contacts will be loaded asynchronously on app start. Depending on quantity and server speed, it might take a few minutes until all contacts +## are loaded. +## Example (all fields are mandatory unless specified as optional): + +## Each block (= CardDAV source) must have the name "carddav", followed by one or more digits. +# [carddav0] + +## The host is the main host address of the remote server, without any port, protocol or path data. +# host=cloud.mycompany.com + +## The path relative to the host were to find the resource. +# path=/remote.php/dav/addressbooks/users/john/contacts/ + +## The user name required for the authentication. +# user=john + +## The server port. +## (optional, defaults: 443 is used if useSSL is true, 80 if false) +# port=443 + +## Whether to use SSL (https) or not (http). +## (optional, default: true) +# useSSL=true + + diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index f320f35..e86ae87 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,4 +1,4 @@ -find_package(Qt6 REQUIRED COMPONENTS Core Widgets Quick QuickControls2 Multimedia Sql Core5Compat) +find_package(Qt6 REQUIRED COMPONENTS Core Widgets Quick QuickControls2 Multimedia Sql Core5Compat Xml) find_package(Qca-qt6 REQUIRED) include(FindPkgConfig) @@ -86,6 +86,35 @@ set(gonnect_qml_singletons ) set_source_files_properties(${gonnect_qml_singletons} PROPERTIES QT_QML_SINGLETON_TYPE TRUE) +if(BUILD_DEPENDENCIES) + include(ExternalProject) + + ExternalProject_Add(qtwebdav + GIT_REPOSITORY https://github.com/PikachuHy/QtWebDAV.git + CMAKE_ARGS -DCMAKE_PREFIX_PATH=${QT6_INSTALL_PREFIX} -DCMAKE_INSTALL_PREFIX= + PATCH_COMMAND git apply "${PROJECT_SOURCE_DIR}/resources/flatpak/patches/qtwebdav-cmake.patch" + UPDATE_DISCONNECTED TRUE + ) + ExternalProject_Get_Property(qtwebdav INSTALL_DIR) + target_include_directories(gonnect SYSTEM PRIVATE ${INSTALL_DIR}/include) + target_link_directories(gonnect PRIVATE ${INSTALL_DIR}/${CMAKE_INSTALL_LIBDIR}) + + ExternalProject_Add(vcard + GIT_REPOSITORY https://github.com/ivanenko/vCard.git + CMAKE_ARGS -DCMAKE_INSTALL_PREFIX= + PATCH_COMMAND git apply "${PROJECT_SOURCE_DIR}/resources/flatpak/patches/vcard-cmake.patch" + UPDATE_DISCONNECTED TRUE + ) + ExternalProject_Get_Property(vcard INSTALL_DIR) + target_include_directories(gonnect SYSTEM PRIVATE ${INSTALL_DIR}/include) + target_link_directories(gonnect PRIVATE ${INSTALL_DIR}/${CMAKE_INSTALL_LIBDIR}) + + add_dependencies(gonnect vcard qtwebdav) +else() + find_package(QtWebDAV REQUIRED) + find_package(vCard REQUIRED) +endif() + qt_add_qml_module(gonnect URI base @@ -107,7 +136,7 @@ qt_add_qml_module(gonnect ui/components/controls/SearchDial.qml ui/components/controls/Switch.qml - ui/components/dialogs/AccountCredentialsDialog.qml + ui/components/dialogs/CredentialsDialog.qml ui/components/dialogs/BaseDialog.qml ui/components/dialogs/ConfirmDialog.qml ui/components/dialogs/InfoDialog.qml @@ -245,6 +274,8 @@ qt_add_qml_module(gonnect contacts/AddressBookManager.h contacts/AvatarManager.cpp contacts/AvatarManager.h + contacts/CardDAVAddressBookFeeder.h + contacts/CardDAVAddressBookFeeder.cpp contacts/Contact.cpp contacts/Contact.h contacts/ContactSerializer.cpp @@ -345,6 +376,7 @@ target_link_libraries(gonnect Qt6::Multimedia Qt6::Sql Qt6::Core5Compat + Qt6::Xml ${PJSIP_STATIC_LIBRARIES} PkgConfig::LIBUUID PkgConfig::LIBUSB1 @@ -352,6 +384,8 @@ target_link_libraries(gonnect GOnnectVersion qca-qt6 hid-rp + vCard + QtWebDAV ) target_include_directories(gonnect diff --git a/src/Main.qml b/src/Main.qml index 6e72fd7..6fa3337 100644 --- a/src/Main.qml +++ b/src/Main.qml @@ -71,12 +71,18 @@ Item { viewHelperConnections.emergencyWindow = null } } + + function onCardDavPasswordRequested(id : string, host : string) { + const dialog = DialogFactory.createDialog("CredentialsDialog.qml", { text: qsTr("Please enter the password for %1:").arg(host) }) + dialog.onPasswordAccepted.connect(pw => ViewHelper.respondCardDavPassword(id, pw)) + } } Connections { target: SIPAccountManager function onAuthorizationFailed(accountId : string) { - DialogFactory.createDialog("AccountCredentialsDialog.qml", { accountId }) + const dialog = DialogFactory.createDialog("CredentialsDialog.qml", { text: qsTr("Please enter the password for the SIP account:") }) + dialog.onPasswordAccepted.connect(pw => SIPAccountManager.setAccountCredentials(accountId, pw)) } } diff --git a/src/contacts/AddressBook.cpp b/src/contacts/AddressBook.cpp index cd8550d..8201fcd 100644 --- a/src/contacts/AddressBook.cpp +++ b/src/contacts/AddressBook.cpp @@ -13,9 +13,9 @@ QString AddressBook::hashifyCn(const QString &cn) const return QCryptographicHash::hash(cn.toUtf8(), QCryptographicHash::Sha256).toHex(); } -void AddressBook::addContact(const QString &dn, const QString &name, const QString &company, - const QString &mail, const QDateTime &lastModified, - const QList &phoneNumbers) +Contact *AddressBook::addContact(const QString &dn, const QString &name, const QString &company, + const QString &mail, const QDateTime &lastModified, + const QList &phoneNumbers) { const auto hid = hashifyCn(dn); @@ -30,6 +30,8 @@ void AddressBook::addContact(const QString &dn, const QString &name, const QStri contact->setMail(mail); contact->setLastModified(lastModified); contact->addPhoneNumbers(phoneNumbers); + + return contact; } void AddressBook::addContact(Contact *contact) diff --git a/src/contacts/AddressBook.h b/src/contacts/AddressBook.h index 70f8a78..f1f1012 100644 --- a/src/contacts/AddressBook.h +++ b/src/contacts/AddressBook.h @@ -19,9 +19,9 @@ class AddressBook : public QObject return *_instance; }; - void addContact(const QString &dn, const QString &name, const QString &company, - const QString &mail, const QDateTime &lastModified, - const QList &phoneNumbers); + Contact *addContact(const QString &dn, const QString &name, const QString &company, + const QString &mail, const QDateTime &lastModified, + const QList &phoneNumbers); void addContact(Contact *contact); diff --git a/src/contacts/AddressBookManager.cpp b/src/contacts/AddressBookManager.cpp index 8f7d8b4..e04e904 100644 --- a/src/contacts/AddressBookManager.cpp +++ b/src/contacts/AddressBookManager.cpp @@ -3,8 +3,12 @@ #include "NetworkHelper.h" #include "AddressBook.h" #include "LDAPAddressBookFeeder.h" +#include "CardDAVAddressBookFeeder.h" #include "CsvFileAddressBookFeeder.h" #include "AvatarManager.h" +#include "ViewHelper.h" +#include "SecretPortal.h" +#include "KeychainSettings.h" #include #include @@ -22,12 +26,14 @@ void AddressBookManager::initAddressBookConfigs() { static QRegularExpression ldapGroupRegex = QRegularExpression("^ldap[0-9]+$"); static QRegularExpression csvGroupRegex = QRegularExpression("^csv[0-9]+$"); + static QRegularExpression carddavGroupRegex = QRegularExpression("^carddav[0-9]+$"); ReadOnlyConfdSettings settings; const QStringList groups = settings.childGroups(); for (const auto &group : groups) { - if (ldapGroupRegex.match(group).hasMatch() || csvGroupRegex.match(group).hasMatch()) { + if (ldapGroupRegex.match(group).hasMatch() || csvGroupRegex.match(group).hasMatch() + || carddavGroupRegex.match(group).hasMatch()) { m_addressBookConfigs.push_back(group); } } @@ -44,6 +50,7 @@ void AddressBookManager::processAddressBookQueue() { static QRegularExpression ldapGroupRegex = QRegularExpression("^ldap[0-9]+$"); static QRegularExpression csvGroupRegex = QRegularExpression("^csv[0-9]+$"); + static QRegularExpression carddavGroupRegex = QRegularExpression("^carddav[0-9]+$"); QMutableStringListIterator it(m_addressBookQueue); while (it.hasNext()) { @@ -57,6 +64,10 @@ void AddressBookManager::processAddressBookQueue() if (processCSVAddressBookConfig(group)) { it.remove(); } + } else if (carddavGroupRegex.match(group).hasMatch()) { + if (processCardDAVAddressBookConfig(group)) { + it.remove(); + } } } @@ -111,3 +122,82 @@ bool AddressBookManager::processCSVAddressBookConfig(const QString &group) return true; } + +bool AddressBookManager::processCardDAVAddressBookConfig(const QString &group) +{ + + KeychainSettings keychainSettings; + keychainSettings.beginGroup(group); + const QString secret = keychainSettings.value("secret", "").toString(); + keychainSettings.endGroup(); + + if (secret.isEmpty()) { + auto &viewHelper = ViewHelper::instance(); + auto conn = connect(&viewHelper, &ViewHelper::cardDavPasswordResponded, this, + [group, this](const QString &id, const QString &password) { + if (id == group) { + QObject::disconnect(m_viewHelperConnections.value(group)); + m_viewHelperConnections.remove(group); + + auto &secretPortal = SecretPortal::instance(); + if (secretPortal.isValid()) { + KeychainSettings settings; + settings.beginGroup(group); + const auto secret = secretPortal.encrypt(password); + settings.setValue("secret", secret); + settings.endGroup(); + } + + processCardDAVAddressBookConfigImpl(group, password); + } + }); + + m_viewHelperConnections.insert(group, conn); + + ReadOnlyConfdSettings settings; + settings.beginGroup(group); + viewHelper.requestCardDavPassword(group, settings.value("host", "").toString()); + settings.endGroup(); + + } else { + auto &secretPortal = SecretPortal::instance(); + if (secretPortal.isValid()) { + if (secretPortal.isInitialized()) { + processCardDAVAddressBookConfigImpl(group, secretPortal.decrypt(secret)); + } else { + connect(&secretPortal, &SecretPortal::initializedChanged, this, + [this, group, secret]() { + processCardDAVAddressBookConfigImpl( + group, SecretPortal::instance().decrypt(secret)); + }); + } + } + } + + return true; +} + +void AddressBookManager::processCardDAVAddressBookConfigImpl(const QString &group, + const QString &password) +{ + ReadOnlyConfdSettings settings; + settings.beginGroup(group); + + QHash settingsHash; + const auto keys = settings.allKeys(); + for (const auto &key : keys) { + qCritical() << key << settings.value(key); + settingsHash.insert(key, settings.value(key, "").toString()); + } + const auto controlHash = qHash(settingsHash); + + const bool useSSL = settings.value("useSSL", true).toBool(); + + auto feeder = new CardDAVAddressBookFeeder( + controlHash, settings.value("host", "").toString(), + settings.value("path", "").toString(), settings.value("user", "").toString(), password, + settings.value("port", useSSL ? 443 : 80).toInt(), useSSL, this); + feeder->feedAddressBook(AddressBook::instance()); + Q_UNUSED(feeder) + settings.endGroup(); +} diff --git a/src/contacts/AddressBookManager.h b/src/contacts/AddressBookManager.h index 8f9cd57..c5e79c7 100644 --- a/src/contacts/AddressBookManager.h +++ b/src/contacts/AddressBookManager.h @@ -1,6 +1,7 @@ #pragma once #include +#include class AddressBookManager : public QObject { @@ -25,7 +26,10 @@ class AddressBookManager : public QObject void processAddressBookQueue(); bool processLDAPAddressBookConfig(const QString &group); bool processCSVAddressBookConfig(const QString &group); + bool processCardDAVAddressBookConfig(const QString &group); + void processCardDAVAddressBookConfigImpl(const QString &group, const QString &password); QStringList m_addressBookConfigs; QStringList m_addressBookQueue; + QHash m_viewHelperConnections; }; diff --git a/src/contacts/AvatarManager.cpp b/src/contacts/AvatarManager.cpp index 81a7e61..284b78d 100644 --- a/src/contacts/AvatarManager.cpp +++ b/src/contacts/AvatarManager.cpp @@ -83,6 +83,26 @@ QString AvatarManager::avatarPathFor(const QString &id) return res; } +void AvatarManager::addExternalImage(const QString &id, const QByteArray &data, + const QDateTime &modified) +{ + const auto dbModified = modifiedTimeInDb(id); + + if (dbModified.isValid() && dbModified < modified) { + createFile(id, data); + updateAvatarModifiedTime(id, modified); + } else if (dbModified.isNull()) { + createFile(id, data); + QHash tmp; + tmp.insert(id, modified); + addIdsToDb(tmp); + } + + if (auto contact = AddressBook::instance().lookupByContactId(id)) { + contact->setHasAvatar(true); + } +} + void AvatarManager::clearCStringlist(char **attrs) const { char **p; @@ -141,7 +161,7 @@ void AvatarManager::updateAvatarModifiedTime(const QString &id, const QDateTime auto db = QSqlDatabase::database(); if (!db.open()) { - qCCritical(lcAvatarManager) << "Unable to open avatars databse:" << db.lastError().text(); + qCCritical(lcAvatarManager) << "Unable to open avatars database:" << db.lastError().text(); } else { qCInfo(lcAvatarManager) << "Successfully opened avatars database"; @@ -159,13 +179,42 @@ void AvatarManager::updateAvatarModifiedTime(const QString &id, const QDateTime } } +QDateTime AvatarManager::modifiedTimeInDb(const QString &id) const +{ + auto db = QSqlDatabase::database(); + if (!db.open()) { + qCCritical(lcAvatarManager) << "Unable to open avatars database:" << db.lastError().text(); + return QDateTime(); + } else { + QSqlQuery query(db); + query.prepare("SELECT lastModified FROM avatars WHERE id = :id;"); + query.bindValue(":id", id); + + if (!query.exec()) { + qCCritical(lcAvatarManager) + << "Error on executing SQL query:" << query.lastError().text(); + return QDateTime(); + } else { + if (query.size() > 1) { + qCCritical(lcAvatarManager) << "Error: id" << id << "is ambigous"; + return QDateTime(); + } else { + query.next(); + return QDateTime::fromSecsSinceEpoch(query.value("lastModified").toLongLong()); + } + } + } + + return QDateTime(); +} + QHash AvatarManager::readIdsFromDb() const { QHash result; auto db = QSqlDatabase::database(); if (!db.open()) { - qCCritical(lcAvatarManager) << "Unable to open avatars databse:" << db.lastError().text(); + qCCritical(lcAvatarManager) << "Unable to open avatars database:" << db.lastError().text(); } else { qCInfo(lcAvatarManager) << "Successfully opened avatars database"; diff --git a/src/contacts/AvatarManager.h b/src/contacts/AvatarManager.h index 3cfacd0..34dac5d 100644 --- a/src/contacts/AvatarManager.h +++ b/src/contacts/AvatarManager.h @@ -22,11 +22,14 @@ class AvatarManager : public QObject QString avatarPathFor(const QString &id); + void addExternalImage(const QString &id, const QByteArray &data, const QDateTime &modified); + private: void clearCStringlist(char **attrs) const; void createFile(const QString &id, const QByteArray &data) const; void addIdsToDb(QHash &idTimeMap) const; void updateAvatarModifiedTime(const QString &id, const QDateTime &modified) const; + QDateTime modifiedTimeInDb(const QString &id) const; QHash readIdsFromDb() const; void loadAvatars(const QList &contacts, const QString &ldapUrl, const QString &ldapBase, const QString &ldapFilter); diff --git a/src/contacts/CardDAVAddressBookFeeder.cpp b/src/contacts/CardDAVAddressBookFeeder.cpp new file mode 100644 index 0000000..ccdb81a --- /dev/null +++ b/src/contacts/CardDAVAddressBookFeeder.cpp @@ -0,0 +1,280 @@ +#include "CardDAVAddressBookFeeder.h" +#include "AddressBook.h" +#include "AvatarManager.h" + +#include +#include +#include + +#include +#include + +#define CARDDAV_MAGIC 0x0891 +#define CARDDAV_VERSION 0x00 + +Q_LOGGING_CATEGORY(lcCardDAVAddressBookFeeder, "gonnect.app.feeder.CardDAVAddressBookFeeder") + +using namespace std::chrono_literals; + +CardDAVAddressBookFeeder::CardDAVAddressBookFeeder(const size_t settingsHash, const QString &host, + const QString &path, const QString &user, + const QString &password, int port, bool useSSL, + QObject *parent) + : QObject{ parent }, m_settingsHash{ settingsHash } +{ + m_cacheWriteTimer.setSingleShot(true); + m_cacheWriteTimer.setInterval(3s); + m_cacheWriteTimer.callOnTimeout(this, &CardDAVAddressBookFeeder::flushCachImpl); + + loadCachedData(settingsHash); + + m_webdav.setConnectionSettings(useSSL ? QWebdav::HTTPS : QWebdav::HTTP, host, path, user, + password, port); + connect(&m_webdavParser, &QWebdavDirParser::finished, this, + &CardDAVAddressBookFeeder::onParserFinished); + connect(&m_webdavParser, &QWebdavDirParser::errorChanged, this, + &CardDAVAddressBookFeeder::onError); + connect(&m_webdav, &QWebdav::errorChanged, this, &CardDAVAddressBookFeeder::onError); +} + +void CardDAVAddressBookFeeder::feedAddressBook(AddressBook &addressBook) +{ + m_addressBook = QPointer(&addressBook); + m_webdavParser.listDirectory(&m_webdav, "/"); +} + +void CardDAVAddressBookFeeder::processVcard(QByteArray data, const QString &uuid, + const QDateTime &modifiedDate) +{ + Q_ASSERT(!m_addressBook.isNull()); + + std::istringstream stringStream(data.toStdString()); + TextReader reader(stringStream); + auto cards = reader.parseCards(); + + for (auto &card : cards) { + auto &props = card.properties(); + + QString uid; + QString name; + QString org; + QString email; + QByteArray photoData; + QList phoneNumbers; + + for (auto &prop : props) { + const auto propName = prop.getName(); + + if (propName == "UID") { + uid = QString::fromStdString(prop.getValue()); + } else if (propName == "FN") { + name = QString::fromStdString(prop.getValue()); + } else if (propName == "ORG") { + org = QString::fromStdString(prop.getValue()); + } else if (propName == "EMAIL") { + email = QString::fromStdString(prop.getValue()); + } else if (propName == "TEL") { + phoneNumbers.append({ Contact::NumberType::Unknown, + QString::fromStdString(prop.getValue()), false }); + } else if (propName == "PHOTO" || propName.starts_with("PHOTO:data:image/jpeg")) { + photoData = QByteArray::fromStdString(prop.getValue()); + } + } + + if (!uuid.isEmpty() && !name.isEmpty() && !phoneNumbers.isEmpty()) { + Contact *contact = + m_addressBook->addContact(uid, name, org, email, modifiedDate, phoneNumbers); + m_cachedContacts.insert(uuid, contact); + + processPhotoProperty(uid, photoData, modifiedDate); + + } else if (!uuid.isEmpty()) { + m_ignoredIds.insert(uuid, modifiedDate); + } + } + + if (!m_cacheWriteTimer.isActive()) { + m_cacheWriteTimer.start(); + } +} + +void CardDAVAddressBookFeeder::loadCachedData(const size_t hash) +{ + if (!hash) { + return; + } + + const auto filePath = cacheFilePath(hash); + QFile cacheFile(filePath); + + if (!cacheFile.exists()) { + return; + } + + if (!cacheFile.open(QIODevice::ReadOnly)) { + onError(QString("Unable to open file %1").arg(filePath)); + return; + } + + QDataStream in(&cacheFile); + + quint16 magic; + quint8 version; + qsizetype numberOfContacts; + + in >> magic; + in >> version; + in >> m_ignoredIds; + in >> numberOfContacts; + + if (magic != CARDDAV_MAGIC || version != CARDDAV_VERSION) { + qCInfo(lcCardDAVAddressBookFeeder) << "CardDAV cache file at" << filePath + << "in invalid and will therefore be removed."; + cacheFile.remove(); + return; + } + + for (qsizetype i = 0; i < numberOfContacts; ++i) { + QString key; + Contact *contact = new Contact(this); + in >> key; + in >> *contact; + m_cachedContacts.insert(key, contact); + } + + cacheFile.close(); + + qCInfo(lcCardDAVAddressBookFeeder) + << "Loaded" << m_cachedContacts.size() << "contacts from cache with hash" << hash; +} + +QString CardDAVAddressBookFeeder::cacheFilePath(const size_t hash, bool createPath) +{ + const QString path = + QString("%1/cache/carddav") + .arg(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)); + + if (createPath) { + QDir dir = QDir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)); + dir.mkpath(path); + } + + return QString("%1/%2.dat").arg(path).arg(hash); +} + +void CardDAVAddressBookFeeder::processPhotoProperty(const QString &id, const QByteArray &data, + const QDateTime &modifiedDate) const +{ + + // Detect vCard version + // 2.1: PHOTO;JPEG;ENCODING=BASE64:[base64-data] + // 3.0: PHOTO;TYPE=JPEG;ENCODING=b:[base64-data] + // 4.0: PHOTO:data:image/jpeg;base64,[base64-data] + + const auto splitted = data.split(':'); + if (splitted.size() != 2) { + return; + } + + // Checking vCard 2.1 and 3.0 + const auto head = splitted.at(0).split(';'); + QByteArray base64Str; + bool isJpeg = false; + bool isBase64 = false; + + for (const auto &part : head) { + if (part == "TYPE=JPEG" || part == "JPEG") { + isJpeg = true; + } else if (part == "ENCODING=b" || part == "ENCODING=BASE64") { + isBase64 = true; + } + } + + if (isJpeg && isBase64) { + base64Str = splitted.at(1); + } else if (data.startsWith("base64,")) { + // Must be vCard 4.0 + base64Str = data.sliced(7); + } + + if (base64Str.isEmpty()) { + return; + } + + // Convert base64 data to image + const QByteArray decoded = QByteArray::fromBase64(base64Str); + AvatarManager::instance().addExternalImage(id, decoded, modifiedDate); +} + +void CardDAVAddressBookFeeder::onError(QString error) const +{ + qCCritical(lcCardDAVAddressBookFeeder) << "Error:" << error; +} + +void CardDAVAddressBookFeeder::onParserFinished() +{ + const auto list = m_webdavParser.getList(); + for (const auto &item : list) { + const auto cacheId = item.name(); + const auto modifiedDate = item.lastModified(); + + if (m_ignoredIds.contains(cacheId) && m_ignoredIds.value(cacheId) >= modifiedDate) { + continue; + } else if (Contact *cashedContact = m_cachedContacts.value(cacheId, nullptr)) { + if (cashedContact->lastModified() >= modifiedDate) { + m_addressBook->addContact(cashedContact); + continue; + } + } + + QNetworkReply *reply = m_webdav.get(item.path()); + connect( + reply, &QNetworkReply::readyRead, this, + [reply, cacheId, modifiedDate, this]() { + if (!reply || !m_addressBook) { + return; + } + QByteArray data = reply->readAll(); + reply->deleteLater(); + + QMimeDatabase db; + QMimeType type = db.mimeTypeForData(data); + if (type.name() == "text/vcard") { + processVcard(data, cacheId, modifiedDate); + } + }, + Qt::ConnectionType::SingleShotConnection); + } +} + +void CardDAVAddressBookFeeder::flushCachImpl() +{ + const auto filePath = cacheFilePath(m_settingsHash, true); + QFile cacheFile(filePath); + + if (!cacheFile.open(QIODevice::WriteOnly)) { + onError(QString("Unable to open file %1").arg(filePath)); + return; + } + + QDataStream out(&cacheFile); + out << quint16(CARDDAV_MAGIC); + out << quint8(CARDDAV_VERSION); + out << m_ignoredIds; + out << m_cachedContacts.size(); + + QHashIterator it(std::as_const(m_cachedContacts)); + while (it.hasNext()) { + it.next(); + out << it.key() << *(it.value()); + } + + cacheFile.close(); + + qCInfo(lcCardDAVAddressBookFeeder) + << m_cachedContacts.size() << "contacts written to CardDAV cache with hash" + << m_settingsHash; +} + +#undef CARDDAV_MAGIC +#undef CARDDAV_VERSION diff --git a/src/contacts/CardDAVAddressBookFeeder.h b/src/contacts/CardDAVAddressBookFeeder.h new file mode 100644 index 0000000..d70bfe4 --- /dev/null +++ b/src/contacts/CardDAVAddressBookFeeder.h @@ -0,0 +1,43 @@ +#pragma once + +#include +#include +#include +#include + +#include "Contact.h" + +class AddressBook; + +class CardDAVAddressBookFeeder : public QObject, public IAddressBookFeeder +{ + Q_OBJECT + +public: + explicit CardDAVAddressBookFeeder(const size_t settingsHash, const QString &host, + const QString &path, const QString &user, + const QString &password, int port, bool useSSL, + QObject *parent = nullptr); + + virtual void feedAddressBook(AddressBook &addressBook) override; + +private slots: + void onError(QString error) const; + void onParserFinished(); + void flushCachImpl(); + +private: + void processVcard(QByteArray data, const QString &uuid, const QDateTime &modifiedDate); + void loadCachedData(const size_t hash); + QString cacheFilePath(const size_t hash, bool createPath = false); + void processPhotoProperty(const QString &id, const QByteArray &data, + const QDateTime &modifiedDate) const; + + size_t m_settingsHash = 0; + QWebdav m_webdav; + QWebdavDirParser m_webdavParser; + QPointer m_addressBook; + QHash m_cachedContacts; + QTimer m_cacheWriteTimer; + QHash m_ignoredIds; +}; diff --git a/src/contacts/Contact.cpp b/src/contacts/Contact.cpp index 3e03570..6b36298 100644 --- a/src/contacts/Contact.cpp +++ b/src/contacts/Contact.cpp @@ -7,6 +7,8 @@ #endif #include +Contact::Contact(QObject *parent) : QObject{ parent } { } + Contact::Contact(const QString &id, const QString &dn, const QString &name, QObject *parent) : QObject{ parent }, m_id{ id }, m_dn{ dn }, m_name{ name } { diff --git a/src/contacts/Contact.h b/src/contacts/Contact.h index 2478084..df6992a 100644 --- a/src/contacts/Contact.h +++ b/src/contacts/Contact.h @@ -26,6 +26,7 @@ class Contact : public QObject bool operator!=(const PhoneNumber &other) const; }; + explicit Contact(QObject *parent = nullptr); explicit Contact(const QString &id, const QString &dn, const QString &name, QObject *parent = nullptr); explicit Contact(const QString &id, const QString &dn, const QString &name, diff --git a/src/media/AudioPort.cpp b/src/media/AudioPort.cpp index d41e79a..232b38d 100644 --- a/src/media/AudioPort.cpp +++ b/src/media/AudioPort.cpp @@ -70,6 +70,7 @@ bool AudioPort::initFmt() } m_pj_fmt.init(formatID, clockRate, channelCount, frameTimeUsec, bitsPerSample); + m_pj_fmt.type = PJMEDIA_TYPE_AUDIO; m_audioFormat = format; return true; @@ -259,9 +260,7 @@ void AudioPort::onFrameReceived(pj::MediaFrame &frame) return; } - if (frame.size) { - m_io->write(reinterpret_cast(frame.buf.data()), frame.size); - } + m_io->write(reinterpret_cast(frame.buf.data()), frame.size); // Auto destroy sink after timeout emit startIdleTimer(); diff --git a/src/ui/ViewHelper.cpp b/src/ui/ViewHelper.cpp index c321a77..aa8be64 100644 --- a/src/ui/ViewHelper.cpp +++ b/src/ui/ViewHelper.cpp @@ -188,3 +188,13 @@ QString ViewHelper::encryptSecret(const QString &secret) const Q_ASSERT(sp.isValid()); return sp.encrypt(secret); } + +void ViewHelper::requestCardDavPassword(const QString &id, const QString &host) +{ + emit cardDavPasswordRequested(id, host); +} + +void ViewHelper::respondCardDavPassword(const QString &id, const QString password) +{ + emit cardDavPasswordResponded(id, password); +} diff --git a/src/ui/ViewHelper.h b/src/ui/ViewHelper.h index 69872e1..8d921a9 100644 --- a/src/ui/ViewHelper.h +++ b/src/ui/ViewHelper.h @@ -72,6 +72,9 @@ class ViewHelper : public QObject Q_INVOKABLE QString encryptSecret(const QString &secret) const; + void requestCardDavPassword(const QString &id, const QString &host); + Q_INVOKABLE void respondCardDavPassword(const QString &id, const QString password); + public slots: Q_INVOKABLE void quitApplication(); @@ -91,6 +94,9 @@ public slots: void showQuitConfirm(); void showEmergency(QString accountId, int callId, QString displayName); void hideEmergency(); + + void cardDavPasswordRequested(QString id, QString host); + void cardDavPasswordResponded(QString id, QString password); }; class ViewHelperWrapper diff --git a/src/ui/components/dialogs/AccountCredentialsDialog.qml b/src/ui/components/dialogs/CredentialsDialog.qml similarity index 83% rename from src/ui/components/dialogs/AccountCredentialsDialog.qml rename to src/ui/components/dialogs/CredentialsDialog.qml index 5c08238..f182437 100644 --- a/src/ui/components/dialogs/AccountCredentialsDialog.qml +++ b/src/ui/components/dialogs/CredentialsDialog.qml @@ -7,14 +7,15 @@ import base BaseDialog { id: control - title: qsTr("Authentication failed") - required property string accountId + signal passwordAccepted(string password) + + property alias text: contentLabel.text Label { id: contentLabel - text: qsTr("Please enter the password for the SIP account:") + text: qsTr("Please enter the password:") elide: Label.ElideRight wrapMode: Label.WordWrap anchors { @@ -52,7 +53,8 @@ BaseDialog { } onClicked: () => { - SIPAccountManager.setAccountCredentials(control.accountId, passwordField.text) + okButton.enabled = false + control.passwordAccepted(passwordField.text) control.close() } } From cd6a7845d5eda14aed4b41743359f07463358e16 Mon Sep 17 00:00:00 2001 From: Markus Bader Date: Wed, 29 Jan 2025 09:59:57 +0100 Subject: [PATCH 02/12] chore: fixed parsing of photo data in CardDAV feeder --- src/contacts/CardDAVAddressBookFeeder.cpp | 75 +++++++++++------------ 1 file changed, 36 insertions(+), 39 deletions(-) diff --git a/src/contacts/CardDAVAddressBookFeeder.cpp b/src/contacts/CardDAVAddressBookFeeder.cpp index ccdb81a..b4b47b3 100644 --- a/src/contacts/CardDAVAddressBookFeeder.cpp +++ b/src/contacts/CardDAVAddressBookFeeder.cpp @@ -48,6 +48,22 @@ void CardDAVAddressBookFeeder::processVcard(QByteArray data, const QString &uuid { Q_ASSERT(!m_addressBook.isNull()); + // Check vCard version first - only 3.0 is supported atm + static const QRegularExpression versionRegExp("VERSION:(?[0-9.]+?)(\\r\\n?|\\n)"); + + const auto matchResult = versionRegExp.match(data); + if (!matchResult.hasMatch()) { + qCritical() << "Cannot parse version from vCard - ignoring"; + return; + } + const auto version = matchResult.captured("version"); + if (version != "3.0") { + qCritical() << "Only vCard version 3.0 is supported at the moment but found" << version + << "- ignoring"; + return; + } + + // Process vcard std::istringstream stringStream(data.toStdString()); TextReader reader(stringStream); auto cards = reader.parseCards(); @@ -76,8 +92,25 @@ void CardDAVAddressBookFeeder::processVcard(QByteArray data, const QString &uuid } else if (propName == "TEL") { phoneNumbers.append({ Contact::NumberType::Unknown, QString::fromStdString(prop.getValue()), false }); - } else if (propName == "PHOTO" || propName.starts_with("PHOTO:data:image/jpeg")) { - photoData = QByteArray::fromStdString(prop.getValue()); + } else if (propName == "PHOTO") { + + auto propParams = prop.params(); + const auto end = propParams.end(); + + bool isBase64 = false; + bool isJpeg = false; + + for (auto it = propParams.begin(); it != end; ++it) { + if (it->first == "ENCODING" && it->second == "b") { + isBase64 = true; + } else if (it->first == "TYPE" && it->second == "JPEG") { + isJpeg = true; + } + } + + if (isBase64 && isJpeg) { + photoData = QByteArray::fromStdString(prop.getValue()); + } } } @@ -165,44 +198,8 @@ QString CardDAVAddressBookFeeder::cacheFilePath(const size_t hash, bool createPa void CardDAVAddressBookFeeder::processPhotoProperty(const QString &id, const QByteArray &data, const QDateTime &modifiedDate) const { - - // Detect vCard version - // 2.1: PHOTO;JPEG;ENCODING=BASE64:[base64-data] - // 3.0: PHOTO;TYPE=JPEG;ENCODING=b:[base64-data] - // 4.0: PHOTO:data:image/jpeg;base64,[base64-data] - - const auto splitted = data.split(':'); - if (splitted.size() != 2) { - return; - } - - // Checking vCard 2.1 and 3.0 - const auto head = splitted.at(0).split(';'); - QByteArray base64Str; - bool isJpeg = false; - bool isBase64 = false; - - for (const auto &part : head) { - if (part == "TYPE=JPEG" || part == "JPEG") { - isJpeg = true; - } else if (part == "ENCODING=b" || part == "ENCODING=BASE64") { - isBase64 = true; - } - } - - if (isJpeg && isBase64) { - base64Str = splitted.at(1); - } else if (data.startsWith("base64,")) { - // Must be vCard 4.0 - base64Str = data.sliced(7); - } - - if (base64Str.isEmpty()) { - return; - } - // Convert base64 data to image - const QByteArray decoded = QByteArray::fromBase64(base64Str); + const QByteArray decoded = QByteArray::fromBase64(data); AvatarManager::instance().addExternalImage(id, decoded, modifiedDate); } From 41ff7440fee25a05c4b71e8c7dc8fe5b9ba802f3 Mon Sep 17 00:00:00 2001 From: Cajus Pollmeier Date: Thu, 30 Jan 2025 10:16:27 +0100 Subject: [PATCH 03/12] fix: remove direct jpeg deps --- src/contacts/AvatarManager.cpp | 25 +++++++++++++++-------- src/contacts/CardDAVAddressBookFeeder.cpp | 6 ++++-- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/contacts/AvatarManager.cpp b/src/contacts/AvatarManager.cpp index 284b78d..9b2d2e4 100644 --- a/src/contacts/AvatarManager.cpp +++ b/src/contacts/AvatarManager.cpp @@ -9,6 +9,7 @@ #include #include #include +#include Q_LOGGING_CATEGORY(lcAvatarManager, "gonnect.app.AvatarManager") @@ -74,7 +75,11 @@ void AvatarManager::initialLoad(const QString &ldapUrl, const QString &ldapBase, QString AvatarManager::avatarPathFor(const QString &id) { - QString res = QString("%1/%2.jpg").arg(m_avatarImageDirPath, id); + if (id.contains("3F-61F51480-A3-C282F80")) { + qCritical() << "so"; + } + + QString res = QString("%1/%2").arg(m_avatarImageDirPath, id); if (!res.isEmpty() && !QFile::exists(res)) { return ""; @@ -116,11 +121,11 @@ void AvatarManager::clearCStringlist(char **attrs) const void AvatarManager::createFile(const QString &id, const QByteArray &data) const { - QFile file(QString("%1/%2.jpg").arg(m_avatarImageDirPath, id)); + QFile file(QString("%1/%2").arg(m_avatarImageDirPath, id)); if (!file.open(QIODevice::WriteOnly)) { qCCritical(lcAvatarManager) - << "Cannot open file" << QString("%1/%2.jpg").arg(m_avatarImageDirPath, id); + << "Cannot open file" << QString("%1/%2").arg(m_avatarImageDirPath, id); return; } @@ -255,17 +260,19 @@ void AvatarManager::loadAvatars(const QList &contacts, const QS QStringList AvatarManager::readContactIdsFromDir() const { + QMimeDatabase db; + QDir avatarDir(m_avatarImageDirPath); - const auto fileList = avatarDir.entryList( - { "*.jpg" }, QDir::Files | QDir::Readable | QDir::NoDotAndDotDot | QDir::NoSymLinks); + const auto fileList = avatarDir.entryList(QDir::Files | QDir::Readable | QDir::NoDotAndDotDot + | QDir::NoSymLinks); QStringList resultList; - for (const auto &fileInfo : fileList) { - const auto fileName = fileInfo; + for (const auto &fileName : fileList) { - if (fileName.length() == 64 + 4) { // File name scheme: uuid (64 chars) + ".jpg" - resultList.append(fileName.left(64)); + QMimeType mime = db.mimeTypeForFile(m_avatarImageDirPath + "/" + fileName); + if (mime.inherits("image/jpeg") || mime.inherits("image/png")) { + resultList.append(fileName); } } diff --git a/src/contacts/CardDAVAddressBookFeeder.cpp b/src/contacts/CardDAVAddressBookFeeder.cpp index b4b47b3..a21ed22 100644 --- a/src/contacts/CardDAVAddressBookFeeder.cpp +++ b/src/contacts/CardDAVAddressBookFeeder.cpp @@ -199,8 +199,10 @@ void CardDAVAddressBookFeeder::processPhotoProperty(const QString &id, const QBy const QDateTime &modifiedDate) const { // Convert base64 data to image - const QByteArray decoded = QByteArray::fromBase64(data); - AvatarManager::instance().addExternalImage(id, decoded, modifiedDate); + if (data.size()) { + const QByteArray decoded = QByteArray::fromBase64(data); + AvatarManager::instance().addExternalImage(id, decoded, modifiedDate); + } } void CardDAVAddressBookFeeder::onError(QString error) const From 7636a6e772aa1eb979d8a8b6d151208e0b6896be Mon Sep 17 00:00:00 2001 From: Cajus Pollmeier Date: Mon, 10 Feb 2025 07:21:45 +0100 Subject: [PATCH 04/12] Adjust links --- resources/flatpak/de.gonicus.gonnect.metainfo.xml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/resources/flatpak/de.gonicus.gonnect.metainfo.xml b/resources/flatpak/de.gonicus.gonnect.metainfo.xml index bd53adc..525974d 100644 --- a/resources/flatpak/de.gonicus.gonnect.metainfo.xml +++ b/resources/flatpak/de.gonicus.gonnect.metainfo.xml @@ -90,9 +90,10 @@ phone - https://www.gonicus.de - https://www.gonicus.de - https://www.gonicus.de + https://github.com/gonicus/gonnect + https://github.com/gonicus/gonnect/issues + https://github.com/gonicus/gonnect/wiki + https://www.gonicus.de gonnect From 0c146bba913aa1f81fd17035024f4d87d74aa35a Mon Sep 17 00:00:00 2001 From: Cajus Pollmeier Date: Tue, 11 Feb 2025 12:58:48 +0100 Subject: [PATCH 05/12] Enable software clock --- src/sip/SIPMediaConfig.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sip/SIPMediaConfig.cpp b/src/sip/SIPMediaConfig.cpp index 7284577..c425411 100644 --- a/src/sip/SIPMediaConfig.cpp +++ b/src/sip/SIPMediaConfig.cpp @@ -20,6 +20,7 @@ void SIPMediaConfig::applyConfig(pj::EpConfig &epConfig) bool noVad = settings.value("media/noVad", false).toBool(); epConfig.medConfig.noVad = noVad; + epConfig.medConfig.sndUseSwClock = true; unsigned clockRate = settings.value("media/clockRate", PJSUA_DEFAULT_CLOCK_RATE).toUInt(&ok); if (ok) { From 690a04cee61cb3095596f8c62cff066f1779472b Mon Sep 17 00:00:00 2001 From: Cajus Pollmeier Date: Tue, 11 Feb 2025 15:50:09 +0100 Subject: [PATCH 06/12] feat: move to software clock by default and make it configurable --- resources/templates/sample.conf | 3 +++ sample.conf | 3 +++ src/sip/SIPMediaConfig.cpp | 4 +++- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/resources/templates/sample.conf b/resources/templates/sample.conf index a76b1e2..fdfcc88 100644 --- a/resources/templates/sample.conf +++ b/resources/templates/sample.conf @@ -123,6 +123,9 @@ maxCalls=4 ## default: false #noVad=false +## Use a software clock to synchronize media streaming. May lead to better latency. +#softwareClock=true + ## Clock rate to be applied to the conference bridge. If value is zero, default clock rate will be used (PJSUA_DEFAULT_CLOCK_RATE, which by default is 16KHz). ## default: 16000 #clockRate=16000 diff --git a/sample.conf b/sample.conf index c5b4fee..e9a2041 100644 --- a/sample.conf +++ b/sample.conf @@ -118,6 +118,9 @@ ## default: false #noVad=false +## Use a software clock to synchronize media streaming. May lead to better latency. +#softwareClock=true + ## Clock rate to be applied to the conference bridge. If value is zero, default clock rate will be used (PJSUA_DEFAULT_CLOCK_RATE, which by default is 16KHz). ## default: 16000 #clockRate=16000 diff --git a/src/sip/SIPMediaConfig.cpp b/src/sip/SIPMediaConfig.cpp index c425411..784ec5e 100644 --- a/src/sip/SIPMediaConfig.cpp +++ b/src/sip/SIPMediaConfig.cpp @@ -20,7 +20,9 @@ void SIPMediaConfig::applyConfig(pj::EpConfig &epConfig) bool noVad = settings.value("media/noVad", false).toBool(); epConfig.medConfig.noVad = noVad; - epConfig.medConfig.sndUseSwClock = true; + + bool useSoftwareClock = settings.value("media/softwareClock", true).toBool(); + epConfig.medConfig.sndUseSwClock = useSoftwareClock; unsigned clockRate = settings.value("media/clockRate", PJSUA_DEFAULT_CLOCK_RATE).toUInt(&ok); if (ok) { From f8a6f8f9d23529864afd1e0fd374f57c45a09e3a Mon Sep 17 00:00:00 2001 From: Markus Bader Date: Tue, 11 Feb 2025 16:11:25 +0100 Subject: [PATCH 07/12] fix: send refresh signals after carddav contacts loaded refs #11 --- src/contacts/CardDAVAddressBookFeeder.cpp | 2 ++ src/contacts/PhoneNumberUtil.cpp | 2 ++ src/ui/HistoryModel.cpp | 3 +++ 3 files changed, 7 insertions(+) diff --git a/src/contacts/CardDAVAddressBookFeeder.cpp b/src/contacts/CardDAVAddressBookFeeder.cpp index a21ed22..8b2599f 100644 --- a/src/contacts/CardDAVAddressBookFeeder.cpp +++ b/src/contacts/CardDAVAddressBookFeeder.cpp @@ -273,6 +273,8 @@ void CardDAVAddressBookFeeder::flushCachImpl() qCInfo(lcCardDAVAddressBookFeeder) << m_cachedContacts.size() << "contacts written to CardDAV cache with hash" << m_settingsHash; + + AddressBook::instance().contactsReady(); } #undef CARDDAV_MAGIC diff --git a/src/contacts/PhoneNumberUtil.cpp b/src/contacts/PhoneNumberUtil.cpp index ad53116..4a1723e 100644 --- a/src/contacts/PhoneNumberUtil.cpp +++ b/src/contacts/PhoneNumberUtil.cpp @@ -194,6 +194,8 @@ PhoneNumberUtil::PhoneNumberUtil() : QObject() { connect(&AddressBook::instance(), &AddressBook::contactsCleared, this, [this]() { m_contactInfoCache.clear(); }); + connect(&AddressBook::instance(), &AddressBook::contactsReady, this, + [this]() { m_contactInfoCache.clear(); }); #ifndef APP_TESTS connect(&AvatarManager::instance(), &AvatarManager::avatarsLoaded, this, diff --git a/src/ui/HistoryModel.cpp b/src/ui/HistoryModel.cpp index a0f0755..e388754 100644 --- a/src/ui/HistoryModel.cpp +++ b/src/ui/HistoryModel.cpp @@ -5,6 +5,7 @@ #include "AvatarManager.h" #include "NumberStats.h" #include "SIPCallManager.h" +#include "AddressBook.h" HistoryModel::HistoryModel(QObject *parent) : QAbstractListModel{ parent } { @@ -32,6 +33,8 @@ HistoryModel::HistoryModel(QObject *parent) : QAbstractListModel{ parent } connect(&numStats, &NumberStats::favoriteRemoved, this, &HistoryModel::handleFavoriteToggle); connect(&numStats, &NumberStats::modelReset, this, &HistoryModel::resetModel); + connect(&AddressBook::instance(), &AddressBook::contactsReady, this, &HistoryModel::resetModel); + connect(this, &HistoryModel::limitChanged, this, &HistoryModel::resetModel); } From 87d5f8fdd7019d84fa2a154e4eab20d87acb82e4 Mon Sep 17 00:00:00 2001 From: Markus Bader Date: Wed, 12 Feb 2025 09:02:28 +0100 Subject: [PATCH 08/12] fix(ui): send update signals when adding avatar via CardDAV refs #11 --- src/contacts/AvatarManager.cpp | 2 ++ src/contacts/AvatarManager.h | 1 + src/contacts/CardDAVAddressBookFeeder.cpp | 1 - src/ui/HistoryModel.cpp | 7 +++++++ 4 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/contacts/AvatarManager.cpp b/src/contacts/AvatarManager.cpp index 9b2d2e4..1bab378 100644 --- a/src/contacts/AvatarManager.cpp +++ b/src/contacts/AvatarManager.cpp @@ -106,6 +106,8 @@ void AvatarManager::addExternalImage(const QString &id, const QByteArray &data, if (auto contact = AddressBook::instance().lookupByContactId(id)) { contact->setHasAvatar(true); } + + emit avatarAdded(id); } void AvatarManager::clearCStringlist(char **attrs) const diff --git a/src/contacts/AvatarManager.h b/src/contacts/AvatarManager.h index 34dac5d..ed9af03 100644 --- a/src/contacts/AvatarManager.h +++ b/src/contacts/AvatarManager.h @@ -42,4 +42,5 @@ class AvatarManager : public QObject signals: void avatarsLoaded(); + void avatarAdded(QString contactId); }; diff --git a/src/contacts/CardDAVAddressBookFeeder.cpp b/src/contacts/CardDAVAddressBookFeeder.cpp index 8b2599f..ead772e 100644 --- a/src/contacts/CardDAVAddressBookFeeder.cpp +++ b/src/contacts/CardDAVAddressBookFeeder.cpp @@ -261,7 +261,6 @@ void CardDAVAddressBookFeeder::flushCachImpl() out << quint8(CARDDAV_VERSION); out << m_ignoredIds; out << m_cachedContacts.size(); - QHashIterator it(std::as_const(m_cachedContacts)); while (it.hasNext()) { it.next(); diff --git a/src/ui/HistoryModel.cpp b/src/ui/HistoryModel.cpp index e388754..e02d6b6 100644 --- a/src/ui/HistoryModel.cpp +++ b/src/ui/HistoryModel.cpp @@ -24,6 +24,13 @@ HistoryModel::HistoryModel(QObject *parent) : QAbstractListModel{ parent } startIndex, endIndex, { static_cast(Roles::HasAvatar), static_cast(Roles::AvatarPath) }); }); + connect(&AvatarManager::instance(), &AvatarManager::avatarAdded, this, [this](QString) { + const auto startIndex = createIndex(0, 0); + const auto endIndex = createIndex(rowCount(QModelIndex()), 0); + emit dataChanged( + startIndex, endIndex, + { static_cast(Roles::HasAvatar), static_cast(Roles::AvatarPath) }); + }); connect(&SIPCallManager::instance(), &SIPCallManager::blocksChanged, this, &HistoryModel::resetModel); From 575f1f3925860a7de48a2fa4ce4de2e7b20b2f2a Mon Sep 17 00:00:00 2001 From: Markus Bader Date: Wed, 12 Feb 2025 10:36:52 +0100 Subject: [PATCH 09/12] refactor: use correct id for CardDAV-loaded contacts refs #11 --- src/contacts/CardDAVAddressBookFeeder.cpp | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/contacts/CardDAVAddressBookFeeder.cpp b/src/contacts/CardDAVAddressBookFeeder.cpp index ead772e..54305d6 100644 --- a/src/contacts/CardDAVAddressBookFeeder.cpp +++ b/src/contacts/CardDAVAddressBookFeeder.cpp @@ -71,7 +71,6 @@ void CardDAVAddressBookFeeder::processVcard(QByteArray data, const QString &uuid for (auto &card : cards) { auto &props = card.properties(); - QString uid; QString name; QString org; QString email; @@ -81,9 +80,7 @@ void CardDAVAddressBookFeeder::processVcard(QByteArray data, const QString &uuid for (auto &prop : props) { const auto propName = prop.getName(); - if (propName == "UID") { - uid = QString::fromStdString(prop.getValue()); - } else if (propName == "FN") { + if (propName == "FN") { name = QString::fromStdString(prop.getValue()); } else if (propName == "ORG") { org = QString::fromStdString(prop.getValue()); @@ -116,10 +113,10 @@ void CardDAVAddressBookFeeder::processVcard(QByteArray data, const QString &uuid if (!uuid.isEmpty() && !name.isEmpty() && !phoneNumbers.isEmpty()) { Contact *contact = - m_addressBook->addContact(uid, name, org, email, modifiedDate, phoneNumbers); + m_addressBook->addContact(uuid, name, org, email, modifiedDate, phoneNumbers); m_cachedContacts.insert(uuid, contact); - processPhotoProperty(uid, photoData, modifiedDate); + processPhotoProperty(contact->id(), photoData, modifiedDate); } else if (!uuid.isEmpty()) { m_ignoredIds.insert(uuid, modifiedDate); @@ -273,7 +270,7 @@ void CardDAVAddressBookFeeder::flushCachImpl() << m_cachedContacts.size() << "contacts written to CardDAV cache with hash" << m_settingsHash; - AddressBook::instance().contactsReady(); + emit AddressBook::instance().contactsReady(); } #undef CARDDAV_MAGIC From 76f63ea46db020277906935d5b5cc580000b3e04 Mon Sep 17 00:00:00 2001 From: Markus Bader Date: Wed, 12 Feb 2025 11:23:12 +0100 Subject: [PATCH 10/12] fix: load avatars of contacts that are created later refs #11 --- src/contacts/AddressBook.cpp | 4 ++++ src/contacts/AddressBook.h | 1 + src/contacts/AvatarManager.cpp | 32 ++++++++++++++++++++++++++++++++ src/contacts/AvatarManager.h | 9 +++++++-- 4 files changed, 44 insertions(+), 2 deletions(-) diff --git a/src/contacts/AddressBook.cpp b/src/contacts/AddressBook.cpp index 8201fcd..322263b 100644 --- a/src/contacts/AddressBook.cpp +++ b/src/contacts/AddressBook.cpp @@ -31,6 +31,8 @@ Contact *AddressBook::addContact(const QString &dn, const QString &name, const Q contact->setLastModified(lastModified); contact->addPhoneNumbers(phoneNumbers); + emit contactAdded(contact); + return contact; } @@ -39,6 +41,8 @@ void AddressBook::addContact(Contact *contact) if (contact != nullptr && !m_contacts.contains(contact->id())) { contact->setParent(this); m_contacts.insert(contact->id(), contact); + + emit contactAdded(contact); } } diff --git a/src/contacts/AddressBook.h b/src/contacts/AddressBook.h index f1f1012..9af5478 100644 --- a/src/contacts/AddressBook.h +++ b/src/contacts/AddressBook.h @@ -42,6 +42,7 @@ class AddressBook : public QObject QHash m_contacts; signals: + void contactAdded(Contact *contact); void contactsCleared(); void contactsReady(); }; diff --git a/src/contacts/AvatarManager.cpp b/src/contacts/AvatarManager.cpp index 1bab378..7eb9fc1 100644 --- a/src/contacts/AvatarManager.cpp +++ b/src/contacts/AvatarManager.cpp @@ -13,6 +13,8 @@ Q_LOGGING_CATEGORY(lcAvatarManager, "gonnect.app.AvatarManager") +using namespace std::chrono_literals; + AvatarManager::AvatarManager(QObject *parent) : QObject{ parent } { const auto baseDirPath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); @@ -23,6 +25,36 @@ AvatarManager::AvatarManager(QObject *parent) : QObject{ parent } baseDir.mkpath("avatars"); qCInfo(lcAvatarManager) << "Created avatar image directory" << m_avatarImageDirPath; } + + m_updateContactsTimer.setSingleShot(true); + m_updateContactsTimer.setInterval(5ms); + m_updateContactsTimer.callOnTimeout(this, &AvatarManager::updateContacts); + + connect(&AddressBook::instance(), &AddressBook::contactAdded, this, [this](Contact *contact) { + m_contactsWithPendingUpdates.append(contact); + + if (!m_updateContactsTimer.isActive()) { + m_updateContactsTimer.start(); + } + }); +} + +void AvatarManager::updateContacts() +{ + if (m_contactsWithPendingUpdates.isEmpty()) { + return; + } + + const auto contactIds = readContactIdsFromDir(); + + for (auto contactPtr : std::as_const(m_contactsWithPendingUpdates)) { + if (contactPtr && contactIds.contains(contactPtr->id())) { + contactPtr->setHasAvatar(true); + } + } + + m_contactsWithPendingUpdates.clear(); + emit avatarsLoaded(); } void AvatarManager::initialLoad(const QString &ldapUrl, const QString &ldapBase, diff --git a/src/contacts/AvatarManager.h b/src/contacts/AvatarManager.h index ed9af03..2af0c91 100644 --- a/src/contacts/AvatarManager.h +++ b/src/contacts/AvatarManager.h @@ -1,8 +1,8 @@ #pragma once #include - -class Contact; +#include +#include "Contact.h" class AvatarManager : public QObject { @@ -39,6 +39,11 @@ class AvatarManager : public QObject explicit AvatarManager(QObject *parent = nullptr); QString m_avatarImageDirPath; + QTimer m_updateContactsTimer; + QList> m_contactsWithPendingUpdates; + +private slots: + void updateContacts(); signals: void avatarsLoaded(); From 979a45ce3b8e408277d71ebf4677a2a793373d5c Mon Sep 17 00:00:00 2001 From: Cajus Pollmeier Date: Wed, 12 Feb 2025 12:39:06 +0100 Subject: [PATCH 11/12] chore: remove debug info --- src/contacts/AvatarManager.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/contacts/AvatarManager.cpp b/src/contacts/AvatarManager.cpp index 7eb9fc1..d24b913 100644 --- a/src/contacts/AvatarManager.cpp +++ b/src/contacts/AvatarManager.cpp @@ -107,10 +107,6 @@ void AvatarManager::initialLoad(const QString &ldapUrl, const QString &ldapBase, QString AvatarManager::avatarPathFor(const QString &id) { - if (id.contains("3F-61F51480-A3-C282F80")) { - qCritical() << "so"; - } - QString res = QString("%1/%2").arg(m_avatarImageDirPath, id); if (!res.isEmpty() && !QFile::exists(res)) { From 61136c7028535d6a2cd2514f6cd41543d85bbdc5 Mon Sep 17 00:00:00 2001 From: Cajus Pollmeier Date: Wed, 12 Feb 2025 12:41:26 +0100 Subject: [PATCH 12/12] chore: use proper logging channel --- src/contacts/CardDAVAddressBookFeeder.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/contacts/CardDAVAddressBookFeeder.cpp b/src/contacts/CardDAVAddressBookFeeder.cpp index 54305d6..33b9019 100644 --- a/src/contacts/CardDAVAddressBookFeeder.cpp +++ b/src/contacts/CardDAVAddressBookFeeder.cpp @@ -53,12 +53,12 @@ void CardDAVAddressBookFeeder::processVcard(QByteArray data, const QString &uuid const auto matchResult = versionRegExp.match(data); if (!matchResult.hasMatch()) { - qCritical() << "Cannot parse version from vCard - ignoring"; + qCCritical(lcCardDAVAddressBookFeeder) << "Cannot parse version from vCard - ignoring"; return; } const auto version = matchResult.captured("version"); if (version != "3.0") { - qCritical() << "Only vCard version 3.0 is supported at the moment but found" << version + qCCritical(lcCardDAVAddressBookFeeder) << "Only vCard version 3.0 is supported at the moment but found" << version << "- ignoring"; return; }