diff --git a/CHANGELOG.md b/CHANGELOG.md index ecee4b10ad..af46af315f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,11 @@ that bundles liblinphone and its dependencies as git submodules. Please refer to CHANGELOG.md files of submodules (mainly: *liblinphone*, *mediastreamer2*, *ortp*) for the actual changes made to these components. +## Unreleased + +### Added +- Support of permanent activities in the presence. This enables the ability for the presence server to store + activities permanently, even if the PUBLISH expires. ### Changes - Upgrade of Opus source code to version 1.5.2 to activate OSCE (deep PLC and LACE/noLACE). diff --git a/liblinphone/coreapi/private_functions.h b/liblinphone/coreapi/private_functions.h index a95eeee835..ec10b52a98 100644 --- a/liblinphone/coreapi/private_functions.h +++ b/liblinphone/coreapi/private_functions.h @@ -1021,7 +1021,7 @@ LINPHONE_PUBLIC void linphone_core_set_add_admin_information_to_contact(Linphone LinphonePresenceModel * linphone_core_create_presence_model_for_consolidated_presence(BCTBX_UNUSED(LinphoneCore *lc), LinphoneConsolidatedPresence presence); -char *linphone_presence_model_to_xml(LinphonePresenceModel *model); +LINPHONE_PUBLIC char *linphone_presence_model_to_xml(LinphonePresenceModel *model); LinphoneVideoDefinition *linphone_video_definition_new(unsigned int width, unsigned int height, const char *name); diff --git a/liblinphone/include/linphone/api/c-account-params.h b/liblinphone/include/linphone/api/c-account-params.h index 34d838d4e8..2d9a69b99e 100644 --- a/liblinphone/include/linphone/api/c-account-params.h +++ b/liblinphone/include/linphone/api/c-account-params.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2025 Belledonne Communications SARL. + * Copyright (c) 2010-2026 Belledonne Communications SARL. * * This file is part of Liblinphone * (see https://gitlab.linphone.org/BC/public/liblinphone). @@ -1040,6 +1040,27 @@ LINPHONE_PUBLIC bool_t linphone_account_params_dtls_srtp_verify_cert_enabled(con */ LINPHONE_PUBLIC void linphone_account_params_enable_dtls_srtp_verify_cert(LinphoneAccountParams *params, bool_t enable); +/** + * Returns whether the echoed presence subscription is enabled or not. + * The echoed presence is the presence as it is returned by the presence server. If a user has multiple devices, one of + * these devices may set some permanent activities. This echoed presence will reflect these permanent activities on the + * other devices that did not set the permanent activities. + * @param params The #LinphoneAccountParams object. @notnil + * @return TRUE if the echoed presence subscription is enabled. + **/ +LINPHONE_PUBLIC bool_t +linphone_account_params_echoed_presence_subscription_enabled(const LinphoneAccountParams *params); + +/** + * Enables or disables the echoed presence subscription on the given #LinphoneAccountParams. + * Enabling does NOT trigger an additional SUBSCRIBE or PUBLISH; it modifies the existing + * presence SUBSCRIBE (adding local account identity). + * @param params The #LinphoneAccountParams object. @notnil + * @param enable TRUE to enable, FALSE to disable. + */ +LINPHONE_PUBLIC void linphone_account_params_enable_echoed_presence_subscription(LinphoneAccountParams *params, + bool_t enable); + /** * @} */ diff --git a/liblinphone/include/linphone/api/c-account.h b/liblinphone/include/linphone/api/c-account.h index 8ea5f02e97..67161bd9ea 100644 --- a/liblinphone/include/linphone/api/c-account.h +++ b/liblinphone/include/linphone/api/c-account.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2022 Belledonne Communications SARL. + * Copyright (c) 2010-2026 Belledonne Communications SARL. * * This file is part of Liblinphone * (see https://gitlab.linphone.org/BC/public/liblinphone). @@ -389,6 +389,16 @@ LINPHONE_PUBLIC LinphoneConsolidatedPresence linphone_account_get_consolidated_p LINPHONE_PUBLIC void linphone_account_set_consolidated_presence(LinphoneAccount *account, LinphoneConsolidatedPresence presence); +/** + * Gets the echoed presence model for the given account. + * The echoed presence model reflects the presence information received from the presence server, + * including permanent activities, when the echoed presence subscription is enabled. + * @param account The #LinphoneAccount object. @notnil + * @return The #LinphonePresenceModel for the echoed presence, or NULL if echoed presence subscription is not enabled. + * @maybenil + */ +LINPHONE_PUBLIC LinphonePresenceModel *linphone_account_get_echoed_presence_model(const LinphoneAccount *account); + /** * Detect if the given input is a phone number or not. * @param account The #LinphoneAccount object, unused yet but may contain useful data. Can be NULL. @maybenil diff --git a/liblinphone/include/linphone/api/c-presence.h b/liblinphone/include/linphone/api/c-presence.h index 51c634360f..7ed46db933 100644 --- a/liblinphone/include/linphone/api/c-presence.h +++ b/liblinphone/include/linphone/api/c-presence.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2022 Belledonne Communications SARL. + * Copyright (c) 2010-2026 Belledonne Communications SARL. * * This file is part of Liblinphone * (see https://gitlab.linphone.org/BC/public/liblinphone). @@ -584,6 +584,72 @@ LINPHONE_PUBLIC LinphoneStatus linphone_presence_person_add_activities_note(Linp */ LINPHONE_PUBLIC LinphoneStatus linphone_presence_person_clear_activities_notes(LinphonePresencePerson *person); +/** + * Adds a permanent activity to a presence person. + * @param person The #LinphonePresencePerson object for which to add a permanent activity. @notnil + * @param activity The #LinphonePresenceActivity object to add to the person. @notnil + * @return 0 if successful, a value < 0 in case of error. + */ +LINPHONE_PUBLIC LinphoneStatus linphone_presence_person_add_permanent_activity(LinphonePresencePerson *person, + LinphonePresenceActivity *activity); + +/** + * Gets the nth permanent activity of a presence person. + * @param person The #LinphonePresencePerson object to get the activity from. @notnil + * @param index The index of the permanent activity to get (the first activity having the index 0). + * @return A pointer to a #LinphonePresenceActivity object if successful, NULL otherwise. @maybenil + */ +LINPHONE_PUBLIC LinphonePresenceActivity * +linphone_presence_person_get_nth_permanent_activity(const LinphonePresencePerson *person, unsigned int index); + +/** + * Gets the number of permanent activities included in the presence person. + * @param person The #LinphonePresencePerson object to get the number of permanent activities from. @notnil + * @return The number of permanent activities included in the #LinphonePresencePerson object. + */ +LINPHONE_PUBLIC unsigned int linphone_presence_person_get_nb_permanent_activities(const LinphonePresencePerson *person); + +/** + * Clears all the permanent activities of a presence person. + * @param person The #LinphonePresencePerson object for which to clear the permanent activities. @notnil + * @return 0 if successful, a value < 0 in case of error. + */ +LINPHONE_PUBLIC LinphoneStatus linphone_presence_person_clear_permanent_activities(LinphonePresencePerson *person); + +/** + * Adds a permanent activities note to a presence person. + * @param person The #LinphonePresencePerson object for which to add a permanent activities note. @notnil + * @param note The #LinphonePresenceNote object to add to the person. @notnil + * @return 0 if successful, a value < 0 in case of error. + */ +LINPHONE_PUBLIC LinphoneStatus linphone_presence_person_add_permanent_activities_note(LinphonePresencePerson *person, + LinphonePresenceNote *note); + +/** + * Gets the nth permanent activities note of a presence person. + * @param person The #LinphonePresencePerson object to get the activities note from. @notnil + * @param index The index of the permanent activities note to get (the first note having the index 0). + * @return A pointer to a #LinphonePresenceNote object if successful, NULL otherwise. @maybenil + */ +LINPHONE_PUBLIC LinphonePresenceNote * +linphone_presence_person_get_nth_permanent_activities_note(const LinphonePresencePerson *person, unsigned int index); + +/** + * Gets the number of permanent activities notes included in the presence person. + * @param person The #LinphonePresencePerson object to get the number of permanent activities notes from. @notnil + * @return The number of permanent activities notes included in the #LinphonePresencePerson object. + */ +LINPHONE_PUBLIC unsigned int +linphone_presence_person_get_nb_permanent_activities_notes(const LinphonePresencePerson *person); + +/** + * Clears all the permanent activities notes of a presence person. + * @param person The #LinphonePresencePerson object for which to clear the permanent activities notes. @notnil + * @return 0 if successful, a value < 0 in case of error. + */ +LINPHONE_PUBLIC LinphoneStatus +linphone_presence_person_clear_permanent_activities_notes(LinphonePresencePerson *person); + /***************************************************************************** * PRESENCE ACTIVITY FUNCTIONS TO GET ACCESS TO ALL FUNCTIONALITIES * ****************************************************************************/ @@ -730,7 +796,7 @@ LINPHONE_PUBLIC int linphone_presence_model_get_capabilities(const LinphonePrese * @return whether or not the #LinphonePresenceModel object has a given capability. */ LINPHONE_PUBLIC bool_t linphone_presence_model_has_capability(const LinphonePresenceModel *model, - const LinphoneFriendCapability capability); + LinphoneFriendCapability capability); /** * Returns whether or not the #LinphonePresenceModel object has a given capability with a certain version. @@ -740,7 +806,7 @@ LINPHONE_PUBLIC bool_t linphone_presence_model_has_capability(const LinphonePres * @return whether or not the #LinphonePresenceModel object has a given capability with a certain version. */ LINPHONE_PUBLIC bool_t linphone_presence_model_has_capability_with_version(const LinphonePresenceModel *model, - const LinphoneFriendCapability capability, + LinphoneFriendCapability capability, float version); /** @@ -750,8 +816,9 @@ LINPHONE_PUBLIC bool_t linphone_presence_model_has_capability_with_version(const * @param version The wanted version to test. * @return whether or not the #LinphonePresenceModel object has a given capability with a certain version or more. */ -LINPHONE_PUBLIC bool_t linphone_presence_model_has_capability_with_version_or_more( - const LinphonePresenceModel *model, const LinphoneFriendCapability capability, float version); +LINPHONE_PUBLIC bool_t linphone_presence_model_has_capability_with_version_or_more(const LinphonePresenceModel *model, + LinphoneFriendCapability capability, + float version); /** * Returns the version of the capability of a #LinphonePresenceModel. @@ -760,7 +827,7 @@ LINPHONE_PUBLIC bool_t linphone_presence_model_has_capability_with_version_or_mo * @return the version of the capability of a #LinphonePresenceModel or -1.0 if the model has not the capability. */ LINPHONE_PUBLIC float linphone_presence_model_get_capability_version(const LinphonePresenceModel *model, - const LinphoneFriendCapability capability); + LinphoneFriendCapability capability); /** * Increase the reference count of the #LinphonePresenceService object. diff --git a/liblinphone/src/account/account-params.cpp b/liblinphone/src/account/account-params.cpp index 747689ed93..9d49b98ff8 100644 --- a/liblinphone/src/account/account-params.cpp +++ b/liblinphone/src/account/account-params.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2025 Belledonne Communications SARL. + * Copyright (c) 2010-2026 Belledonne Communications SARL. * * This file is part of Liblinphone * (see https://gitlab.linphone.org/BC/public/liblinphone). @@ -20,8 +20,6 @@ #include "account-params.h" -#include - #include "c-wrapper/internal/c-tools.h" #include "core/core.h" #include "linphone/types.h" @@ -220,6 +218,11 @@ AccountParams::AccountParams(LinphoneCore *lc, bool useDefaultValues) { mInstantMessagingEncryptionMandatory = useDefaultValues ? !!linphone_config_get_default_int(lc->config, "proxy", "im_encryption_mandatory", 0) : 0; + mEchoedPresenceSubscriptionEnabled = + useDefaultValues + ? linphone_config_get_default_int(lc->config, "proxy", "echoed_presence_subscription", FALSE) == TRUE + : false; + string supportedTags = lc ? linphone_config_get_default_string(lc->config, "proxy", "supported", "empty") : "empty"; if (useDefaultValues && supportedTags != "empty") { vector splitTags = bctoolbox::Utils::split(supportedTags, ","); @@ -352,6 +355,10 @@ AccountParams::AccountParams(LinphoneCore *lc, int index) : AccountParams(lc, fa setPictureUri(linphone_config_get_string(config, key, "picture_uri", mPictureUri.c_str())); + enableEchoedPresenceSubscription(linphone_config_get_bool(config, key, "echoed_presence_subscription", + echoedPresenceSubscriptionEnabled() ? TRUE : FALSE) == + TRUE); + string mwiServerUri = linphone_config_get_string(config, key, "mwi_server_uri", ""); mMwiServerAddress = nullptr; if (!mwiServerUri.empty()) { @@ -470,6 +477,8 @@ AccountParams::AccountParams(const AccountParams &other) : HybridObject(other), mSupportedTagsList = other.mSupportedTagsList; mUseSupportedTags = other.mUseSupportedTags; + + mEchoedPresenceSubscriptionEnabled = other.mEchoedPresenceSubscriptionEnabled; } AccountParams::~AccountParams() { @@ -1046,6 +1055,14 @@ std::shared_ptr AccountParams::getVoicemailAddress() const { return mVoicemailAddress; } +bool AccountParams::echoedPresenceSubscriptionEnabled() const { + return mEchoedPresenceSubscriptionEnabled; +} + +void AccountParams::enableEchoedPresenceSubscription(bool enabled) { + mEchoedPresenceSubscriptionEnabled = enabled; +} + // ----------------------------------------------------------------------------- LinphoneStatus AccountParams::setServerAddress(const std::shared_ptr &serverAddr) { @@ -1268,6 +1285,8 @@ void AccountParams::writeToConfigFile(LinphoneConfig *config, int index) { if (mVoicemailAddress) { linphone_config_set_string(config, key, "voicemail_uri", getVoicemailAddressCstr()); } + linphone_config_set_bool(config, key, "echoed_presence_subscription", + mEchoedPresenceSubscriptionEnabled ? TRUE : FALSE); linphone_config_set_bool(config, key, "im_encryption_mandatory", mInstantMessagingEncryptionMandatory); diff --git a/liblinphone/src/account/account-params.h b/liblinphone/src/account/account-params.h index 17df05df03..a11fb28930 100644 --- a/liblinphone/src/account/account-params.h +++ b/liblinphone/src/account/account-params.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2025 Belledonne Communications SARL. + * Copyright (c) 2010-2026 Belledonne Communications SARL. * * This file is part of Liblinphone * (see https://gitlab.linphone.org/BC/public/liblinphone). @@ -25,7 +25,6 @@ #include "address/address.h" #include "c-wrapper/list-holder.h" -#include "linphone/api/c-push-notification-config.h" #include "linphone/api/c-types.h" #include "linphone/types.h" #include "utils/custom-params.h" @@ -104,6 +103,7 @@ public: void setInstantMessagingEncryptionMandatory(bool mandatory); void setSupportedTagsList(const std::list &supportedTagsList); void enableDtlsSrtpVerifyCert(bool flag); + void enableEchoedPresenceSubscription(bool enabled); // Getters int getExpires() const; @@ -165,6 +165,7 @@ public: const bctbx_list_t *getSupportedTagsCList() const; bool useSupportedTags() const; bool dtlsSrtpVerifyCertEnabled() const; + bool echoedPresenceSubscriptionEnabled() const; // Other LinphoneStatus setServerAddress(const std::shared_ptr &serverAddr); @@ -206,6 +207,7 @@ private: bool mDtlsSrtpVerifyCertEnabled; bool mAllowCpimMessagesInBasicChatRooms; bool mInstantMessagingEncryptionMandatory; + bool mEchoedPresenceSubscriptionEnabled; void *mUserData; diff --git a/liblinphone/src/account/account.cpp b/liblinphone/src/account/account.cpp index edc31e7d33..a2bbaff226 100644 --- a/liblinphone/src/account/account.cpp +++ b/liblinphone/src/account/account.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2025 Belledonne Communications SARL. + * Copyright (c) 2010-2026 Belledonne Communications SARL. * * This file is part of Liblinphone * (see https://gitlab.linphone.org/BC/public/liblinphone). @@ -22,31 +22,21 @@ #include "account.h" +#include "c-wrapper/c-wrapper.h" +#include "c-wrapper/internal/c-tools.h" #include "chat/ics/ics.h" #include "conference/client-conference.h" #include "content/content.h" -#include "core/core.h" -#include "linphone/api/c-account-params.h" -#include "linphone/api/c-account.h" -#include "linphone/api/c-address.h" -#include "linphone/utils/algorithm.h" -#include "push-notification/push-notification-config.h" -#ifdef HAVE_ADVANCED_IM -#ifdef HAVE_LIME_X3DH -#include "chat/encryption/lime-x3dh-encryption-engine.h" -#endif // HAVE_LIME_X3DH -#endif // HAVE_ADVANCED_IM -#include "c-wrapper/c-wrapper.h" -#include "c-wrapper/internal/c-tools.h" #include "core/core-p.h" +#include "core/core.h" #include "db/main-db-p.h" #include "event/event-publish.h" #include "friend/friend.h" #include "linphone/core.h" +#include "linphone/utils/algorithm.h" #include "presence/presence-model.h" #include "presence/presence-service.h" -#include "private.h" -#include "utils/custom-params.h" +#include "push-notification/push-notification-config.h" #include "utils/fsm-integrity-checker.h" #include "utils/xml-utils.h" #ifdef HAVE_XERCESC @@ -1447,6 +1437,14 @@ LinphoneConsolidatedPresence Account::getConsolidatedPresence() const { : LinphoneConsolidatedPresenceOffline; } +const std::shared_ptr &Account::getEchoedPresenceModel() const { + return mEchoedPresenceModel; +} + +void Account::setEchoedPresenceModel(const std::shared_ptr &presenceModel) { + mEchoedPresenceModel = presenceModel; +} + int Account::sendPublish() { if (!mPresenceModel) { lError() << "No presence model has been set for " << *this << ", can't send the PUBLISH"; @@ -1483,7 +1481,7 @@ int Account::sendPublish() { LinphoneConfig *config = linphone_core_get_config(getCCore()); if (linphone_config_get_bool(config, "sip", "update_presence_model_timestamp_before_publish_expires_refresh", - FALSE)) { + FALSE) == TRUE) { unsigned int nbServices = mPresenceModel->getNbServices(); if (nbServices > 0) { std::shared_ptr latestService = mPresenceModel->getNthService(nbServices - 1); @@ -1531,6 +1529,7 @@ int Account::sendPublish() { content->setContentType(contentType); err = mPresencePublishEvent->send(content); + mPresenceModel->hasBeenPublished(); if (presentityAddress) { lInfo() << "Restoring previous presentity address " << *presentityAddress << " for model [" @@ -1538,8 +1537,9 @@ int Account::sendPublish() { mPresenceModel->setPresentity(presentityAddress); mPresenceModel->setContact(contact); } - } else + } else { setSendPublish(true); /*otherwise do not send publish if registration is in progress, this will be done later*/ + } return err; } diff --git a/liblinphone/src/account/account.h b/liblinphone/src/account/account.h index bcc361dda1..216e5c0b32 100644 --- a/liblinphone/src/account/account.h +++ b/liblinphone/src/account/account.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2025 Belledonne Communications SARL. + * Copyright (c) 2010-2026 Belledonne Communications SARL. * * This file is part of Liblinphone * (see https://gitlab.linphone.org/BC/public/liblinphone). @@ -101,6 +101,7 @@ public: void setPresenceModel(const std::shared_ptr &presence, bool needToSendPublish = true); void setConsolidatedPresence(LinphoneConsolidatedPresence presence); void setRemoveAuthInfoOnRegistrationCleared(bool removeAuthOnCleared); + void setEchoedPresenceModel(const std::shared_ptr &presenceModel); // Getters int getAuthFailure() const; @@ -121,6 +122,7 @@ public: LimeUserAccountStatus getLimeUserAccountStatus() const; const std::shared_ptr &getPresenceModel() const; LinphoneConsolidatedPresence getConsolidatedPresence() const; + const std::shared_ptr &getEchoedPresenceModel() const; int getUnreadChatMessageCount() const; const std::list> &getChatRooms() const; @@ -131,7 +133,7 @@ public: std::list> getCallLogs() const; std::list> getCallLogsForAddress(const std::shared_ptr &) const; std::list> - getConferenceInfos(const std::list capabilities = {}) const; + getConferenceInfos(std::list capabilities = {}) const; void addConferenceInfo(const std::shared_ptr &info); void ccmpConferenceInformationResponseReceived(); void ccmpConferenceInformationRequestSent(); @@ -173,7 +175,7 @@ public: static LinphoneAccountAddressComparisonResult compareLinphoneAddresses(const std::shared_ptr &a, const std::shared_ptr &b); - static void writeAllToConfigFile(const std::shared_ptr core); + static void writeAllToConfigFile(std::shared_ptr core); static void writeToConfigFile(LpConfig *config, const std::shared_ptr &account, int index); // To be removed when not using proxy config anymore @@ -252,6 +254,7 @@ private: std::shared_ptr mPresencePublishEvent; std::shared_ptr mPresenceModel; + std::shared_ptr mEchoedPresenceModel; std::shared_ptr mDependency; std::weak_ptr mDependee; @@ -292,7 +295,7 @@ private: class AccountLogContextualizer : public CoreLogContextualizer { public: AccountLogContextualizer(const LinphoneAccount *account) - : CoreLogContextualizer(account ? Account::toCpp(account) : nullptr) { + : CoreLogContextualizer((account != nullptr) ? Account::toCpp(account) : nullptr) { } }; diff --git a/liblinphone/src/c-wrapper/api/c-account-params.cpp b/liblinphone/src/c-wrapper/api/c-account-params.cpp index 8df544eff3..7767fac5e3 100644 --- a/liblinphone/src/c-wrapper/api/c-account-params.cpp +++ b/liblinphone/src/c-wrapper/api/c-account-params.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2025 Belledonne Communications SARL. + * Copyright (c) 2010-2026 Belledonne Communications SARL. * * This file is part of Liblinphone * (see https://gitlab.linphone.org/BC/public/liblinphone). @@ -537,3 +537,11 @@ bool_t linphone_account_params_dtls_srtp_verify_cert_enabled(const LinphoneAccou void linphone_account_params_enable_dtls_srtp_verify_cert(LinphoneAccountParams *params, bool_t enable) { AccountParams::toCpp(params)->enableDtlsSrtpVerifyCert(enable); } + +bool_t linphone_account_params_echoed_presence_subscription_enabled(const LinphoneAccountParams *params) { + return AccountParams::toCpp(params)->echoedPresenceSubscriptionEnabled() ? TRUE : FALSE; +} + +void linphone_account_params_enable_echoed_presence_subscription(LinphoneAccountParams *params, bool_t enable) { + AccountParams::toCpp(params)->enableEchoedPresenceSubscription(enable == TRUE); +} diff --git a/liblinphone/src/c-wrapper/api/c-account.cpp b/liblinphone/src/c-wrapper/api/c-account.cpp index e96459e6f2..e3d6f68595 100644 --- a/liblinphone/src/c-wrapper/api/c-account.cpp +++ b/liblinphone/src/c-wrapper/api/c-account.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2022 Belledonne Communications SARL. + * Copyright (c) 2010-2026 Belledonne Communications SARL. * * This file is part of Liblinphone * (see https://gitlab.linphone.org/BC/public/liblinphone). @@ -25,7 +25,6 @@ #include "account/account-params.h" #include "account/account.h" #include "c-wrapper/c-wrapper.h" -#include "core/core-p.h" #include "dial-plan/dial-plan.h" #include "linphone/api/c-account-cbs.h" #include "linphone/api/c-account-params.h" @@ -304,6 +303,14 @@ void linphone_account_set_consolidated_presence(LinphoneAccount *account, Linpho Account::toCpp(account)->setConsolidatedPresence(presence); } +LinphonePresenceModel *linphone_account_get_echoed_presence_model(const LinphoneAccount *account) { + const auto &presenceModel = Account::toCpp(account)->getEchoedPresenceModel(); + if (presenceModel) { + return presenceModel->toC(); + } + return nullptr; +} + void linphone_account_set_presence_model(LinphoneAccount *account, LinphonePresenceModel *presence_model) { linphone_account_set_presence_model_with_publish_toggle(account, presence_model, TRUE); } diff --git a/liblinphone/src/c-wrapper/api/c-presence.cpp b/liblinphone/src/c-wrapper/api/c-presence.cpp index 836a6aa3ea..b250838b30 100644 --- a/liblinphone/src/c-wrapper/api/c-presence.cpp +++ b/liblinphone/src/c-wrapper/api/c-presence.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2023 Belledonne Communications SARL. + * Copyright (c) 2010-2026 Belledonne Communications SARL. * * This file is part of Liblinphone * (see https://gitlab.linphone.org/BC/public/liblinphone). @@ -18,8 +18,6 @@ * along with this program. If not, see . */ -#include - #include #include @@ -28,7 +26,6 @@ #include "linphone/api/c-address.h" #include "linphone/core.h" #include "linphone/lpconfig.h" -#include "linphone/presence.h" #include "linphone/types.h" #include "presence/presence-activity.h" #include "presence/presence-model.h" @@ -408,6 +405,70 @@ LinphoneStatus linphone_presence_person_clear_activities_notes(LinphonePresenceP return 0; } +unsigned int linphone_presence_person_get_nb_permanent_activities(const LinphonePresencePerson *person) { + if (person == nullptr) { + return 0; + } + return PresencePerson::toCpp(person)->getNbPermanentActivities(); +} + +LinphonePresenceActivity *linphone_presence_person_get_nth_permanent_activity(const LinphonePresencePerson *person, + unsigned int index) { + if (person == nullptr) { + return nullptr; + } + std::shared_ptr activity = PresencePerson::toCpp(person)->getNthPermanentActivity(index); + return activity ? activity->toC() : nullptr; +} + +LinphoneStatus linphone_presence_person_add_permanent_activity(LinphonePresencePerson *person, + LinphonePresenceActivity *activity) { + if (person == nullptr || activity == nullptr) { + return -1; + } + return PresencePerson::toCpp(person)->addPermanentActivity(PresenceActivity::getSharedFromThis(activity)); +} + +LinphoneStatus linphone_presence_person_clear_permanent_activities(LinphonePresencePerson *person) { + if (person == nullptr) { + return -1; + } + PresencePerson::toCpp(person)->clearPermanentActivities(); + return 0; +} + +unsigned int linphone_presence_person_get_nb_permanent_activities_notes(const LinphonePresencePerson *person) { + if (person == nullptr) { + return 0; + } + return PresencePerson::toCpp(person)->getNbPermanentActivitiesNotes(); +} + +LinphonePresenceNote *linphone_presence_person_get_nth_permanent_activities_note(const LinphonePresencePerson *person, + unsigned int index) { + if (person == nullptr) { + return nullptr; + } + std::shared_ptr note = PresencePerson::toCpp(person)->getNthPermanentActivitiesNote(index); + return note ? note->toC() : nullptr; +} + +LinphoneStatus linphone_presence_person_add_permanent_activities_note(LinphonePresencePerson *person, + LinphonePresenceNote *note) { + if (person == nullptr || note == nullptr) { + return -1; + } + return PresencePerson::toCpp(person)->addPermanentActivitiesNote(PresenceNote::getSharedFromThis(note)); +} + +LinphoneStatus linphone_presence_person_clear_permanent_activities_notes(LinphonePresencePerson *person) { + if (person == nullptr) { + return -1; + } + PresencePerson::toCpp(person)->clearPermanentActivitiesNotes(); + return 0; +} + /***************************************************************************** * PRESENCE ACTIVITY FUNCTIONS TO GET ACCESS TO ALL FUNCTIONALITIES * ****************************************************************************/ @@ -707,8 +768,10 @@ void linphone_notify_parse_presence(const char *content_type, const char *content_subtype, const char *body, SalPresenceModel **result) { - PresenceModel::parsePresence(L_C_TO_STRING(content_type), L_C_TO_STRING(content_subtype), L_C_TO_STRING(body), - result); + if (result != nullptr) { + *result = PresenceModel::parsePresence(L_C_TO_STRING(content_type), L_C_TO_STRING(content_subtype), + L_C_TO_STRING(body)); + } } void linphone_notify_recv(LinphoneCore *lc, SalOp *op, SalSubscribeStatus ss, SalPresenceModel *model) { diff --git a/liblinphone/src/friend/friend-list.cpp b/liblinphone/src/friend/friend-list.cpp index 5131ed711b..1c5fe4c402 100644 --- a/liblinphone/src/friend/friend-list.cpp +++ b/liblinphone/src/friend/friend-list.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2023 Belledonne Communications SARL. + * Copyright (c) 2010-2026 Belledonne Communications SARL. * * This file is part of Liblinphone * (see https://gitlab.linphone.org/BC/public/liblinphone). @@ -581,46 +581,79 @@ std::string FriendList::createResourceListXml() const { std::string xmlContent; if (mFriendsMapByUri.empty()) { lWarning() << __FUNCTION__ << ": Empty list in subscription, ignored."; - return std::string(); + return {}; } xmlBufferPtr buf = xmlBufferCreate(); - if (!buf) { + if (buf == nullptr) { lError() << __FUNCTION__ << ": Error creating the XML buffer"; - return std::string(); + return {}; } xmlTextWriterPtr writer = xmlNewTextWriterMemory(buf, 0); - if (!writer) { + if (writer == nullptr) { lError() << __FUNCTION__ << ": Error creating the XML writer"; xmlBufferFree(buf); - return std::string(); + return {}; } xmlTextWriterSetIndent(writer, 1); int err = xmlTextWriterStartDocument(writer, "1.0", "UTF-8", nullptr); - if (err >= 0) + if (err >= 0) { err = xmlTextWriterStartElementNS(writer, nullptr, (const xmlChar *)"resource-lists", (const xmlChar *)"urn:ietf:params:xml:ns:resource-lists"); - if (err >= 0) + } + if (err >= 0) { err = xmlTextWriterWriteAttributeNS(writer, (const xmlChar *)"xmlns", (const xmlChar *)"xsi", nullptr, (const xmlChar *)"http://www.w3.org/2001/XMLSchema-instance"); + } if (err >= 0) { err = xmlTextWriterStartElement(writer, (const xmlChar *)"list"); } + // Add local identify urls if echoed presence subscription is enabled to get the echoed model from the presence + // server. This is to be able to get a coherent view of the permanent activities across all the devices. + for (const auto &account : getCore()->getAccounts()) { + if (account->getAccountParams()->echoedPresenceSubscriptionEnabled()) { + const std::shared_ptr
localIdentity = account->getAccountParams()->getIdentityAddress(); + if (localIdentity && !localIdentity->asStringUriOnly().empty()) { + if (err >= 0) { + err = xmlTextWriterStartElement(writer, (const xmlChar *)"entry"); + } + if (err >= 0) { + err = xmlTextWriterWriteAttribute(writer, (const xmlChar *)"uri", + (const xmlChar *)localIdentity->asStringUriOnly().c_str()); + } + if (err >= 0) { + err = xmlTextWriterEndElement(writer); // Close the "entry" element. + } + } + } + } + std::string previousEntry; for (const auto &entry : mFriendsMapByUri) { // Map is sorted, prevent duplicates if (previousEntry.empty() || (previousEntry != entry.first)) { - if (err >= 0) err = xmlTextWriterStartElement(writer, (const xmlChar *)"entry"); - if (err >= 0) + if (err >= 0) { + err = xmlTextWriterStartElement(writer, (const xmlChar *)"entry"); + } + if (err >= 0) { err = xmlTextWriterWriteAttribute(writer, (const xmlChar *)"uri", (const xmlChar *)entry.first.c_str()); - if (err >= 0) err = xmlTextWriterEndElement(writer); // Close the "entry" element. + } + if (err >= 0) { + err = xmlTextWriterEndElement(writer); // Close the "entry" element. + } } previousEntry = entry.first; } - if (err >= 0) err = xmlTextWriterEndElement(writer); // Close the "list" element. - if (err >= 0) err = xmlTextWriterEndElement(writer); // Close the "resource-lists" element. - if (err >= 0) err = xmlTextWriterEndDocument(writer); + if (err >= 0) { + err = xmlTextWriterEndElement(writer); // Close the "list" element. + } + if (err >= 0) { + err = xmlTextWriterEndElement(writer); // Close the "resource-lists" element. + } + if (err >= 0) { + err = xmlTextWriterEndDocument(writer); + } if (err > 0) { // xmlTextWriterEndDocument returns the size of the content. xmlContent = (char *)buf->content; @@ -634,7 +667,7 @@ std::string FriendList::createResourceListXml() const { std::string FriendList::createResourceListXml() const { lWarning() << "FriendList::createResourceListXml() is stubbed."; - return std::string(); + return {}; } #endif @@ -764,7 +797,7 @@ class FriendListXmlException : public std::exception { public: FriendListXmlException(const char *msg) : mMessage(msg) { } - const char *what() const throw() override { + [[nodiscard]] const char *what() const noexcept override { return mMessage; } @@ -785,7 +818,9 @@ void FriendList::parseMultipartRelatedBody(const std::shared_ptr xmlXPathRegisterNs(xmlCtx.getXpathContext(), reinterpret_cast("rlmi"), reinterpret_cast("urn:ietf:params:xml:ns:rlmi")); std::string versionStr = xmlCtx.getAttributeTextContent("/rlmi:list", "version"); - if (versionStr.empty()) throw FriendListXmlException("rlmi+xml: No version attribute in list"); + if (versionStr.empty()) { + throw FriendListXmlException("rlmi+xml: No version attribute in list"); + } int version = atoi(versionStr.c_str()); if (version < mExpectedNotificationVersion) { // No longer an error as dialog may be silently restarting by the refresher @@ -793,42 +828,54 @@ void FriendList::parseMultipartRelatedBody(const std::shared_ptr << mExpectedNotificationVersion << ", dialog may have been reseted"; } std::string fullStateStr = xmlCtx.getAttributeTextContent("/rlmi:list", "fullState"); - if (fullStateStr.empty()) throw FriendListXmlException("rlmi+xml: No fullState attribute in list"); + if (fullStateStr.empty()) { + throw FriendListXmlException("rlmi+xml: No fullState attribute in list"); + } bool fullState = false; std::string fullStateString(fullStateStr); if ((fullStateString == "true") || (fullStateString == "1")) { fullState = true; - for (const auto &lf : mFriendsList.mList) + for (const auto &lf : mFriendsList.mList) { lf->clearPresenceModels(); + } } - if ((mExpectedNotificationVersion == 0) && !fullState) + if ((mExpectedNotificationVersion == 0) && !fullState) { throw FriendListXmlException("rlmi+xml: Notification with version 0 is not full state, this is not valid"); + } mExpectedNotificationVersion = version + 1; xmlXPathObjectPtr nameObject = xmlCtx.getXpathObjectForNodeList("/rlmi:list/rlmi:resource/rlmi:name/.."); - if (nameObject && nameObject->nodesetval) { + if (nameObject != nullptr && nameObject->nodesetval != nullptr) { for (int i = 1; i <= nameObject->nodesetval->nodeNr; i++) { xmlCtx.setXpathContextNode(xmlXPathNodeSetItem(nameObject->nodesetval, i - 1)); std::string name = xmlCtx.getTextContent("./rlmi:name"); std::string uri = xmlCtx.getTextContent("./@uri"); - if (uri.empty()) continue; + if (uri.empty()) { + continue; + } std::shared_ptr
addr = Address::create(uri); - if (!addr) continue; + if (!addr) { + continue; + } std::shared_ptr lf = findFriendByAddress(addr); if (!lf && mBodylessSubscription) { lf = Friend::create(getCore(), uri); addFriend(lf); } - if (!name.empty()) lf->setName(name); + if (!name.empty()) { + lf->setName(name); + } } } - if (nameObject) xmlXPathFreeObject(nameObject); + if (nameObject != nullptr) { + xmlXPathFreeObject(nameObject); + } std::set> listFriendsPresenceReceived; bctbx_list_t *parts = linphone_content_get_parts(content->toC()); xmlXPathObjectPtr resourceObject = xmlCtx.getXpathObjectForNodeList("/rlmi:list/rlmi:resource/rlmi:instance[@state=\"active\"]/.."); - if (resourceObject && resourceObject->nodesetval) { + if (resourceObject != nullptr && resourceObject->nodesetval != nullptr) { for (int i = 1; i <= resourceObject->nodesetval->nodeNr; i++) { xmlCtx.setXpathContextNode(xmlXPathNodeSetItem(resourceObject->nodesetval, i - 1)); std::string cid = xmlCtx.getTextContent("./rlmi:instance/@cid"); @@ -836,9 +883,9 @@ void FriendList::parseMultipartRelatedBody(const std::shared_ptr std::shared_ptr presencePart = nullptr; bctbx_list_t *it = parts; while (it != nullptr) { - LinphoneContent *content = (LinphoneContent *)it->data; + auto *content = (LinphoneContent *)it->data; const char *header = linphone_content_get_custom_header(content, "Content-Id"); - if (header && Utils::iequalsIgnoreBrakets(header, cid)) { + if (header != nullptr && Utils::iequalsIgnoreBrakets(header, cid)) { presencePart = Content::toCpp(content)->getSharedFromThis(); break; } @@ -847,21 +894,26 @@ void FriendList::parseMultipartRelatedBody(const std::shared_ptr if (!presencePart) { lWarning() << "rlmi+xml: Cannot find part with Content-Id: " << cid; } else { - SalPresenceModel *presence = nullptr; const ContentType &presencePartContentType = presencePart->getContentType(); - PresenceModel::parsePresence(presencePartContentType.getType(), - presencePartContentType.getSubType(), - presencePart->getBodyAsUtf8String(), &presence); - if (presence) { + SalPresenceModel *presence = PresenceModel::parsePresence(presencePartContentType.getType(), + presencePartContentType.getSubType(), + presencePart->getBodyAsUtf8String()); + if (presence != nullptr) { // Try to reduce CPU cost of linphone_address_new and find_friend_by_address by only doing // it when we know for sure we have a presence to notify std::string uri = xmlCtx.getTextContent("./@uri"); - if (uri.empty()) continue; + if (uri.empty()) { + continue; + } std::shared_ptr
addr = Address::create(uri); - if (!addr) continue; + if (!addr) { + continue; + } // Clean the URI - if (addr->hasUriParam("gr")) addr->removeUriParam("gr"); + if (addr->hasUriParam("gr")) { + addr->removeUriParam("gr"); + } uri = addr->asStringUriOnly(); const auto [first, last] = mFriendsMapByUri.equal_range(uri); @@ -873,13 +925,27 @@ void FriendList::parseMultipartRelatedBody(const std::shared_ptr getSharedFromThis(), uri, PresenceModel::toCpp((LinphonePresenceModel *)presence)->getSharedFromThis()); listFriendsPresenceReceived.insert(lf); + } else { + for (const auto &account : getCore()->getAccounts()) { + if (account->getAccountParams()->echoedPresenceSubscriptionEnabled()) { + const std::shared_ptr
localIdentity = + account->getAccountParams()->getIdentityAddress(); + if (localIdentity && localIdentity->asStringUriOnly() == uri) { + account->setEchoedPresenceModel( + PresenceModel::toCpp( + reinterpret_cast(presence)) + ->getSharedFromThis()); + } + } + } } } else { // Save the equal_range iterators for looping because mFriendsMapByUri might // change during the loop, leading to wrong presence notifications std::list>::iterator> its; - for (auto it = first; it != last; it++) + for (auto it = first; it != last; it++) { its.push_back(it); + } for (const auto &it : its) { it->second->presenceReceived( getSharedFromThis(), uri, @@ -897,15 +963,18 @@ void FriendList::parseMultipartRelatedBody(const std::shared_ptr // Notify list with all friends for which we received presence information if (!listFriendsPresenceReceived.empty()) { bctbx_list_t *l = nullptr; - for (const auto &lf : listFriendsPresenceReceived) + for (const auto &lf : listFriendsPresenceReceived) { l = bctbx_list_append(l, lf->toC()); + } LINPHONE_HYBRID_OBJECT_INVOKE_CBS(FriendList, this, linphone_friend_list_cbs_get_presence_received, l); bctbx_list_free(l); } } bctbx_list_free_with_data(parts, (void (*)(void *))linphone_content_unref); - if (resourceObject) xmlXPathFreeObject(resourceObject); + if (resourceObject != nullptr) { + xmlXPathFreeObject(resourceObject); + } } catch (FriendListXmlException &e) { lWarning() << e.what(); } diff --git a/liblinphone/src/presence/presence-model.cpp b/liblinphone/src/presence/presence-model.cpp index 4a7fcae90a..2a78e6f8c5 100644 --- a/liblinphone/src/presence/presence-model.cpp +++ b/liblinphone/src/presence/presence-model.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2023 Belledonne Communications SARL. + * Copyright (c) 2010-2026 Belledonne Communications SARL. * * This file is part of Liblinphone * (see https://gitlab.linphone.org/BC/public/liblinphone). @@ -18,6 +18,13 @@ * along with this program. If not, see . */ +#ifdef _MSC_VER +// Prevent compilation error with std::max, because Visual Studio defines a max macro. +// This define prevents Visual Studio from defining with max macro. +#define NOMINMAX +#endif + +#include #include #include @@ -127,7 +134,9 @@ std::shared_ptr PresenceModel::getActivity() const { /* Suppose that if at least one service is open, then the model is open. */ LinphonePresenceBasicStatus PresenceModel::getBasicStatus() const { for (const auto &service : mServices) { - if (service->getBasicStatus() == LinphonePresenceBasicStatusOpen) return LinphonePresenceBasicStatusOpen; + if (service->getBasicStatus() == LinphonePresenceBasicStatusOpen) { + return LinphonePresenceBasicStatusOpen; + } } return LinphonePresenceBasicStatusClosed; } @@ -137,7 +146,9 @@ int PresenceModel::getCapabilities() const { for (const auto &service : mServices) { for (const auto &pair : service->mCapabilities) { LinphoneFriendCapability capability = Friend::nameToCapability(pair.first); - if (capability != LinphoneFriendCapabilityNone) capabilities |= capability; + if (capability != LinphoneFriendCapabilityNone) { + capabilities |= capability; + } } } return capabilities; @@ -147,15 +158,21 @@ float PresenceModel::getCapabilityVersion(const LinphoneFriendCapability capabil auto it = std::max_element(mServices.cbegin(), mServices.cend(), [&](const auto &a, const auto &b) { return a->getCapabilityVersion(capability) < b->getCapabilityVersion(capability); }); - if (it == mServices.cend()) return -1; + if (it == mServices.cend()) { + return -1; + } return (*it)->getCapabilityVersion(capability); } LinphoneConsolidatedPresence PresenceModel::getConsolidatedPresence() const { - if (isOnline()) return LinphoneConsolidatedPresenceOnline; + if (isOnline()) { + return LinphoneConsolidatedPresenceOnline; + } if (getBasicStatus() == LinphonePresenceBasicStatusClosed) { unsigned int nbActivities = getNbActivities(); - if (nbActivities == 0) return LinphoneConsolidatedPresenceOffline; + if (nbActivities == 0) { + return LinphoneConsolidatedPresenceOffline; + } return LinphoneConsolidatedPresenceDoNotDisturb; } return LinphoneConsolidatedPresenceBusy; @@ -164,7 +181,9 @@ LinphoneConsolidatedPresence PresenceModel::getConsolidatedPresence() const { const std::string &PresenceModel::getContact() const { for (const auto &service : mServices) { const std::string &contact = service->getContact(); - if (!contact.empty()) return contact; + if (!contact.empty()) { + return contact; + } } return emptyString; } @@ -172,12 +191,14 @@ const std::string &PresenceModel::getContact() const { time_t PresenceModel::getLatestActivityTimestamp() const { auto it = std::max_element(mPersons.cbegin(), mPersons.cend(), [](const auto &a, const auto &b) { return a->getTimestamp() < b->getTimestamp(); }); - if (it == mPersons.cend()) return static_cast(-1); + if (it == mPersons.cend()) { + return static_cast(-1); + } return (*it)->getTimestamp(); } unsigned int PresenceModel::getNbActivities() const { - return std::accumulate(mPersons.cbegin(), mPersons.cend(), 0u, + return std::accumulate(mPersons.cbegin(), mPersons.cend(), 0U, [](unsigned int a, const auto &person) { return a + person->getNbActivities(); }); } @@ -192,22 +213,32 @@ unsigned int PresenceModel::getNbServices() const { std::shared_ptr PresenceModel::getNote(const std::string &lang) const { /* First try to find a note in the specified language exactly. */ std::shared_ptr note = findNoteWithLang(lang); - if (note) return note; + if (note) { + return note; + } /* No notes in the specified language has been found, try to find one without language. */ note = findNoteWithLang(std::string()); - if (note) return note; + if (note) { + return note; + } /* Still no result, so get the first note even if it is not in the specified language. */ for (const auto &person : mPersons) { note = person->getNthActivitiesNote(0); - if (note) return note; + if (note) { + return note; + } note = person->getNthNote(0); - if (note) return note; + if (note) { + return note; + } } for (const auto &service : mServices) { note = service->getNthNote(0); - if (note) return note; + if (note) { + return note; + } } return getNthNote(0); } @@ -252,12 +283,12 @@ const std::shared_ptr
&PresenceModel::getPresentity() const { } time_t PresenceModel::getTimestamp() const { - time_t timestamp = static_cast(-1); + auto timestamp = static_cast(-1); for (const auto &service : mServices) { - if (service->getTimestamp() > timestamp) timestamp = service->getTimestamp(); + timestamp = std::max(service->getTimestamp(), timestamp); } for (const auto &person : mPersons) { - if (person->getTimestamp() > timestamp) timestamp = person->getTimestamp(); + timestamp = std::max(person->getTimestamp(), timestamp); } return timestamp; } @@ -265,7 +296,9 @@ time_t PresenceModel::getTimestamp() const { // ----------------------------------------------------------------------------- LinphoneStatus PresenceModel::addActivity(const std::shared_ptr &activity) { - if (!activity) return -1; + if (!activity) { + return -1; + } std::shared_ptr person; if (getNbPersons() == 0) { /* There is no person in the presence model, add one. */ @@ -280,7 +313,9 @@ LinphoneStatus PresenceModel::addActivity(const std::shared_ptr service = getNthService(0); if (!service) { @@ -302,7 +337,9 @@ LinphoneStatus PresenceModel::addNote(const std::string &content, const std::str } LinphoneStatus PresenceModel::addPerson(const std::shared_ptr &person) { - if (!person) return -1; + if (!person) { + return -1; + } auto it = std::lower_bound(mPersons.cbegin(), mPersons.cend(), person->getTimestamp(), [](const auto &person, time_t timestamp) { return person->getTimestamp() < timestamp; }); mPersons.insert(it, person); @@ -310,21 +347,26 @@ LinphoneStatus PresenceModel::addPerson(const std::shared_ptr &p } LinphoneStatus PresenceModel::addService(const std::shared_ptr &service) { - if (!service) return -1; + if (!service) { + return -1; + } mServices.push_back(service); return 0; } void PresenceModel::clearActivities() { - for (auto &person : mPersons) + for (auto &person : mPersons) { person->clearActivities(); + } } void PresenceModel::clearNotes() { - for (auto &person : mPersons) + for (auto &person : mPersons) { person->clearNotes(); - for (auto &service : mServices) + } + for (auto &service : mServices) { service->clearNotes(); + } mNotes.clear(); } @@ -337,68 +379,89 @@ void PresenceModel::clearServices() { } bool PresenceModel::hasCapability(const LinphoneFriendCapability capability) const { - return getCapabilities() & capability; + return (getCapabilities() & capability) != 0; } bool PresenceModel::hasCapabilityWithVersion(const LinphoneFriendCapability capability, float version) const { - for (const auto &service : mServices) { - if (service->hasCapabilityWithVersion(capability, version)) return true; - } - return false; + return std::any_of(mServices.begin(), mServices.end(), [capability, version](const auto &service) { + return service->hasCapabilityWithVersion(capability, version); + }); } bool PresenceModel::hasCapabilityWithVersionOrMore(const LinphoneFriendCapability capability, float version) const { - for (const auto &service : mServices) { - if (service->hasCapabilityWithVersionOrMore(capability, version)) return true; - } - return false; + return std::any_of(mServices.begin(), mServices.end(), [capability, version](const auto &service) { + return service->hasCapabilityWithVersionOrMore(capability, version); + }); } bool PresenceModel::isOnline() const { return (mIsOnline || ((getBasicStatus() == LinphonePresenceBasicStatusOpen) && (getNbActivities() == 0))); } +void PresenceModel::hasBeenPublished() { + for (const auto &person : mPersons) { + person->mPermanentActivitiesToBePublished = false; + } +} + #ifdef HAVE_XML2 int PresenceModel::parsePidfXmlPresenceNotes(XmlParsingContext &xmlContext) { std::stringstream ss; xmlXPathObjectPtr noteObject = xmlContext.getXpathObjectForNodeList("/pidf:presence/pidf:note"); - if (noteObject && noteObject->nodesetval) { + if ((noteObject != nullptr) && (noteObject->nodesetval != nullptr)) { for (int i = 1; i <= noteObject->nodesetval->nodeNr; i++) { ss.clear(), ss.str(std::string()), ss << "/pidf:presence/pidf:note[" << i << "]"; std::string noteStr = xmlContext.getTextContent(ss.str()); - if (noteStr.empty()) continue; + if (noteStr.empty()) { + continue; + } ss.clear(), ss.str(std::string()), ss << "/pidf:presence/pidf:note[" << i << "]/@xml:lang"; std::string lang = xmlContext.getTextContent(ss.str()); std::shared_ptr note = PresenceNote::create(noteStr, lang); mNotes.push_back(note); } } - if (noteObject) xmlXPathFreeObject(noteObject); + if (noteObject != nullptr) { + xmlXPathFreeObject(noteObject); + } return 0; } int PresenceModel::parsePidfXmlPresencePersons(XmlParsingContext &xmlContext) { stringstream ss; - time_t timestamp = static_cast(-1); + auto timestamp = static_cast(-1); int err = 0; - xmlXPathObjectPtr personObject = xmlContext.getXpathObjectForNodeList(PresencePerson::pidfXmlPrefix.data()); - if (personObject && personObject->nodesetval) { + xmlXPathObjectPtr personObject = xmlContext.getXpathObjectForNodeList(std::string(PresencePerson::pidfXmlPrefix)); + if ((personObject != nullptr) && (personObject->nodesetval != nullptr)) { for (int i = 1; i <= personObject->nodesetval->nodeNr; i++) { ss.clear(), ss.str(std::string()), ss << PresencePerson::pidfXmlPrefix.data() << "[" << i << "]/@id"; std::string personIdStr = xmlContext.getTextContent(ss.str()); ss.clear(), ss.str(std::string()), ss << PresencePerson::pidfXmlPrefix.data() << "[" << i << "]/dm:timestamp"; std::string personTimestampStr = xmlContext.getTextContent(ss.str()); - if (!personTimestampStr.empty()) timestamp = PresenceModel::parseTimestamp(personTimestampStr); + if (!personTimestampStr.empty()) { + timestamp = PresenceModel::parseTimestamp(personTimestampStr); + } std::shared_ptr person = PresencePerson::create(personIdStr, timestamp); err = person->parsePidfXmlPresenceActivities(xmlContext, (unsigned int)i); - if (err == 0) err = person->parsePidfXmlPresenceNotes(xmlContext, (unsigned int)i); - if (err == 0) addPerson(person); - if (err != 0) break; + if (err == 0) { + err = person->parsePidfXmlPresencePermanentActivities(xmlContext, (unsigned int)i); + } + if (err == 0) { + err = person->parsePidfXmlPresenceNotes(xmlContext, (unsigned int)i); + } + if (err == 0) { + addPerson(person); + } + if (err != 0) { + break; + } } } - if (personObject) xmlXPathFreeObject(personObject); + if (personObject != nullptr) { + xmlXPathFreeObject(personObject); + } if (err < 0) { /* Remove all the persons added since there was an error. */ clearPersons(); @@ -410,22 +473,30 @@ int PresenceModel::parsePidfXmlPresenceServices(XmlParsingContext &xmlContext) { stringstream ss; LinphonePresenceBasicStatus basicStatus; - xmlXPathObjectPtr serviceObject = xmlContext.getXpathObjectForNodeList(PresenceService::pidfXmlPrefix.data()); - if (serviceObject && serviceObject->nodesetval) { + xmlXPathObjectPtr serviceObject = xmlContext.getXpathObjectForNodeList(std::string(PresenceService::pidfXmlPrefix)); + if ((serviceObject != nullptr) && (serviceObject->nodesetval != nullptr)) { for (int i = 1; i <= serviceObject->nodesetval->nodeNr; i++) { ss.clear(), ss.str(std::string()), ss << PresenceService::pidfXmlPrefix.data() << "[" << i << "]/pidf:status/pidf:basic"; std::string basicStatusStr = xmlContext.getTextContent(ss.str()); - if (basicStatusStr.empty()) continue; - if (basicStatusStr == "open") basicStatus = LinphonePresenceBasicStatusOpen; - else if (basicStatusStr == "closed") basicStatus = LinphonePresenceBasicStatusClosed; - else return -1; /* Invalid value for basic status. */ + if (basicStatusStr.empty()) { + continue; + } + if (basicStatusStr == "open") { + basicStatus = LinphonePresenceBasicStatusOpen; + } else if (basicStatusStr == "closed") { + basicStatus = LinphonePresenceBasicStatusClosed; + } else { + return -1; /* Invalid value for basic status. */ + } ss.clear(), ss.str(std::string()), ss << PresenceService::pidfXmlPrefix.data() << "[" << i << "]/pidf:status/pidfonline:online"; xmlXPathObjectPtr pidfonlineObject = xmlContext.getXpathObjectForNodeList(ss.str()); - if (pidfonlineObject) { - if (pidfonlineObject->nodesetval && pidfonlineObject->nodesetval->nodeNr > 0) mIsOnline = true; + if (pidfonlineObject != nullptr) { + if ((pidfonlineObject->nodesetval != nullptr) && pidfonlineObject->nodesetval->nodeNr > 0) { + mIsOnline = true; + } xmlXPathFreeObject(pidfonlineObject); } @@ -445,8 +516,8 @@ int PresenceModel::parsePidfXmlPresenceServices(XmlParsingContext &xmlContext) { ss << PresenceService::pidfXmlPrefix.data() << "[" << i << "]/oma-pres:service-description"; xmlXPathObjectPtr descriptionsObject = xmlContext.getXpathObjectForNodeList(ss.str()); std::list descriptions; - if (descriptionsObject) { - if (descriptionsObject->nodesetval) { + if (descriptionsObject != nullptr) { + if (descriptionsObject->nodesetval != nullptr) { for (int j = 1; j <= descriptionsObject->nodesetval->nodeNr; j++) { xmlContext.setXpathContextNode(xmlXPathNodeSetItem(descriptionsObject->nodesetval, j - 1)); std::string serviceId = xmlContext.getTextContent("./oma-pres:service-id"); @@ -460,14 +531,22 @@ int PresenceModel::parsePidfXmlPresenceServices(XmlParsingContext &xmlContext) { xmlXPathFreeObject(descriptionsObject); } - if (!timestampStr.empty()) service->setTimestamp(PresenceModel::parseTimestamp(timestampStr)); - if (!contactStr.empty()) service->setContact(contactStr); - if (!descriptions.empty()) service->setDescriptions(descriptions); + if (!timestampStr.empty()) { + service->setTimestamp(PresenceModel::parseTimestamp(timestampStr)); + } + if (!contactStr.empty()) { + service->setContact(contactStr); + } + if (!descriptions.empty()) { + service->setDescriptions(descriptions); + } service->parsePidfXmlPresenceNotes(xmlContext, (unsigned int)i); addService(service); } } - if (serviceObject) xmlXPathFreeObject(serviceObject); + if (serviceObject != nullptr) { + xmlXPathFreeObject(serviceObject); + } return 0; } @@ -482,20 +561,28 @@ std::shared_ptr PresenceModel::findNoteWithLang(const std::string const auto activityNoteIterator = std::find_if(person->mActivitiesNotes.cbegin(), person->mActivitiesNotes.cend(), [&](const auto ¬e) { return note->getLang() == lang; }); - if (activityNoteIterator != person->mActivitiesNotes.cend()) return *activityNoteIterator; + if (activityNoteIterator != person->mActivitiesNotes.cend()) { + return *activityNoteIterator; + } /* ... then look in the person notes. */ const auto noteIterator = std::find_if(person->mNotes.cbegin(), person->mNotes.cend(), [&](const auto ¬e) { return note->getLang() == lang; }); - if (noteIterator != person->mNotes.cend()) return *noteIterator; + if (noteIterator != person->mNotes.cend()) { + return *noteIterator; + } } for (const auto &service : mServices) { const auto it = std::find_if(service->mNotes.cbegin(), service->mNotes.cend(), [&](const auto ¬e) { return note->getLang() == lang; }); - if (it != service->mNotes.cend()) return *it; + if (it != service->mNotes.cend()) { + return *it; + } } const auto noteIterator = std::find_if(mNotes.cbegin(), mNotes.cend(), [&](const auto ¬e) { return note->getLang() == lang; }); - if (noteIterator != mNotes.cend()) return *noteIterator; + if (noteIterator != mNotes.cend()) { + return *noteIterator; + } return nullptr; } @@ -519,12 +606,17 @@ std::string PresenceModel::toXml() const { std::string content; try { - if (!getPresentity()) + if (!getPresentity()) { throw PresenceModelXmlException("Cannot convert presence model to xml because no presentity set"); + } buf = xmlBufferCreate(); - if (!buf) throw PresenceModelXmlException("Error creating the XML buffer"); + if (buf == nullptr) { + throw PresenceModelXmlException("Error creating the XML buffer"); + } writer = xmlNewTextWriterMemory(buf, 0); - if (!writer) throw PresenceModelXmlException("Error creating the XML writer"); + if (writer == nullptr) { + throw PresenceModelXmlException("Error creating the XML writer"); + } xmlTextWriterSetIndent(writer, 1); int err = xmlTextWriterStartDocument(writer, "1.0", "UTF-8", nullptr); @@ -556,18 +648,24 @@ std::string PresenceModel::toXml() const { err = PresenceService::toXml(nullptr, writer, contact, false); } else { for (const auto &service : mServices) { - if (err >= 0) err = service->toXml(writer, contact, isOnline()); + if (err >= 0) { + err = service->toXml(writer, contact, isOnline()); + } } } } if (err >= 0) { for (const auto &person : mPersons) { - if (err >= 0) err = person->toXml(writer); + if (err >= 0) { + err = person->toXml(writer); + } } } if (err >= 0) { for (const auto ¬e : mNotes) { - if (err >= 0) err = note->toXml(writer, ""); + if (err >= 0) { + err = note->toXml(writer, ""); + } } } @@ -586,8 +684,12 @@ std::string PresenceModel::toXml() const { ms_error("%s", e.what()); } - if (writer) xmlFreeTextWriter(writer); - if (buf) xmlBufferFree(buf); + if (writer != nullptr) { + xmlFreeTextWriter(writer); + } + if (buf != nullptr) { + xmlBufferFree(buf); + } return content; } @@ -595,7 +697,7 @@ std::string PresenceModel::toXml() const { std::string PresenceModel::toXml() const { ms_warning("PresenceModel::toXml(): stubbed."); - return std::string(); + return {}; } #endif /* HAVE_XML2 */ @@ -627,33 +729,35 @@ std::string PresenceModel::generatePresenceId() { #ifdef HAVE_XML2 -void PresenceModel::parsePresence(const std::string &contentType, - const std::string &contentSubtype, - const std::string &body, - SalPresenceModel **result) { +SalPresenceModel *PresenceModel::parsePresence(const std::string &contentType, + const std::string &contentSubtype, + const std::string &body) { if (contentType != "application") { - *result = nullptr; - return; + return nullptr; } if (contentSubtype != "pidf+xml") { - *result = nullptr; ms_error("Unknown content type '%s/%s' for presence", L_STRING_TO_C(contentType), L_STRING_TO_C(contentSubtype)); - return; + return nullptr; } std::shared_ptr model = nullptr; XmlParsingContext xmlContext = XmlParsingContext(body); - if (xmlContext.isValid()) model = PresenceModel::parsePidfXmlPresence(xmlContext); - else ms_warning("Wrongly formatted presence XML: %s", xmlContext.getError().c_str()); + if (xmlContext.isValid()) { + model = PresenceModel::parsePidfXmlPresence(xmlContext); + } else { + ms_warning("Wrongly formatted presence XML: %s", xmlContext.getError().c_str()); + } - *result = (SalPresenceModel *)(model ? linphone_presence_model_ref(model->toC()) : nullptr); + return (SalPresenceModel *)(model ? linphone_presence_model_ref(model->toC()) : nullptr); } std::shared_ptr PresenceModel::parsePidfXmlPresence(XmlParsingContext &xmlContext) { int err; - if (xmlContext.createXpathContext() < 0) return nullptr; + if (xmlContext.createXpathContext() < 0) { + return nullptr; + } std::shared_ptr model = PresenceModel::create(); xmlXPathRegisterNs(xmlContext.getXpathContext(), reinterpret_cast("pidf"), @@ -674,7 +778,9 @@ std::shared_ptr PresenceModel::parsePidfXmlPresence(XmlParsingCon err = model->parsePidfXmlPresenceNotes(xmlContext); } - if (err < 0) model = nullptr; + if (err < 0) { + model = nullptr; + } return model; } @@ -708,12 +814,11 @@ time_t PresenceModel::parseTimestamp(const std::string ×tamp) { #else -void PresenceModel::parsePresence(BCTBX_UNUSED(const std::string &contentType), - BCTBX_UNUSED(const std::string &contentSubtype), - BCTBX_UNUSED(const std::string &body), - SalPresenceModel **result) { - if (result) *result = nullptr; +SalPresenceModel *PresenceModel::parsePresence(BCTBX_UNUSED(const std::string &contentType), + BCTBX_UNUSED(const std::string &contentSubtype), + BCTBX_UNUSED(const std::string &body)) { ms_warning("PresenceModel::parsePresence(): stubbed."); + return nullptr; } #endif /* HAVE_XML2 */ @@ -728,7 +833,9 @@ int PresenceModel::timestampToXml(xmlTextWriterPtr writer, time_t timestamp, con err = xmlTextWriterWriteElementNS(writer, (const xmlChar *)L_STRING_TO_C(ns), (const xmlChar *)"timestamp", nullptr, (const xmlChar *)timestamp_str); } - if (timestamp_str) ms_free(timestamp_str); + if (timestamp_str != nullptr) { + ms_free(timestamp_str); + } return err; } #endif /* HAVE_XML2 */ diff --git a/liblinphone/src/presence/presence-model.h b/liblinphone/src/presence/presence-model.h index 2d5a1cb4a9..d825bb3e0e 100644 --- a/liblinphone/src/presence/presence-model.h +++ b/liblinphone/src/presence/presence-model.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2023 Belledonne Communications SARL. + * Copyright (c) 2010-2026 Belledonne Communications SARL. * * This file is part of Liblinphone * (see https://gitlab.linphone.org/BC/public/liblinphone). @@ -82,7 +82,7 @@ public: std::shared_ptr getActivity() const; LinphonePresenceBasicStatus getBasicStatus() const; int getCapabilities() const; - float getCapabilityVersion(const LinphoneFriendCapability capability) const; + float getCapabilityVersion(LinphoneFriendCapability capability) const; LinphoneConsolidatedPresence getConsolidatedPresence() const; const std::string &getContact() const; time_t getLatestActivityTimestamp() const; @@ -106,26 +106,27 @@ public: void clearNotes(); void clearPersons(); void clearServices(); - bool hasCapability(const LinphoneFriendCapability capability) const; - bool hasCapabilityWithVersion(const LinphoneFriendCapability capability, float version) const; - bool hasCapabilityWithVersionOrMore(const LinphoneFriendCapability capability, float version) const; + bool hasCapability(LinphoneFriendCapability capability) const; + bool hasCapabilityWithVersion(LinphoneFriendCapability capability, float version) const; + bool hasCapabilityWithVersionOrMore(LinphoneFriendCapability capability, float version) const; bool isOnline() const; + void hasBeenPublished(); #ifdef HAVE_XML2 int parsePidfXmlPresenceNotes(XmlParsingContext &xmlContext); int parsePidfXmlPresencePersons(XmlParsingContext &xmlContext); int parsePidfXmlPresenceServices(XmlParsingContext &xmlContext); + #endif /* HAVE_XML2 */ + static SalPresenceModel * + parsePresence(const std::string &contentType, const std::string &contentSubtype, const std::string &body); + private: std::shared_ptr findNoteWithLang(const std::string &lang) const; std::string toXml() const; - static std::string basicStatusToString(const LinphonePresenceBasicStatus status); + static std::string basicStatusToString(LinphonePresenceBasicStatus status); static std::string generatePresenceId(); - static void parsePresence(const std::string &contentType, - const std::string &contentSubtype, - const std::string &body, - SalPresenceModel **result); static time_t parseTimestamp(const std::string ×tamp); #ifdef HAVE_XML2 diff --git a/liblinphone/src/presence/presence-person.cpp b/liblinphone/src/presence/presence-person.cpp index 1660207c49..67db661ba8 100644 --- a/liblinphone/src/presence/presence-person.cpp +++ b/liblinphone/src/presence/presence-person.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2023 Belledonne Communications SARL. + * Copyright (c) 2010-2026 Belledonne Communications SARL. * * This file is part of Liblinphone * (see https://gitlab.linphone.org/BC/public/liblinphone). @@ -47,8 +47,11 @@ PresencePerson *PresencePerson::clone() const { // ----------------------------------------------------------------------------- void PresencePerson::setId(const std::string &id) { - if (id.empty()) mId = PresenceModel::generatePresenceId(); - else mId = id; + if (id.empty()) { + mId = PresenceModel::generatePresenceId(); + } else { + mId = id; + } } // ----------------------------------------------------------------------------- @@ -69,7 +72,15 @@ unsigned int PresencePerson::getNbNotes() const { return static_cast(mNotes.size()); } -const std::shared_ptr PresencePerson::getNthActivity(unsigned int idx) const { +unsigned int PresencePerson::getNbPermanentActivities() const { + return static_cast(mPermanentActivities.size()); +} + +unsigned int PresencePerson::getNbPermanentActivitiesNotes() const { + return static_cast(mPermanentActivitiesNotes.size()); +} + +std::shared_ptr PresencePerson::getNthActivity(unsigned int idx) const { try { return mActivities.at(idx); } catch (std::out_of_range &) { @@ -77,7 +88,7 @@ const std::shared_ptr PresencePerson::getNthActivity(unsigned } } -const std::shared_ptr PresencePerson::getNthActivitiesNote(unsigned int idx) const { +std::shared_ptr PresencePerson::getNthActivitiesNote(unsigned int idx) const { try { return mActivitiesNotes.at(idx); } catch (std::out_of_range &) { @@ -85,7 +96,7 @@ const std::shared_ptr PresencePerson::getNthActivitiesNote(unsigne } } -const std::shared_ptr PresencePerson::getNthNote(unsigned int idx) const { +std::shared_ptr PresencePerson::getNthNote(unsigned int idx) const { try { return mNotes.at(idx); } catch (std::out_of_range &) { @@ -93,6 +104,22 @@ const std::shared_ptr PresencePerson::getNthNote(unsigned int idx) } } +std::shared_ptr PresencePerson::getNthPermanentActivity(unsigned int idx) const { + try { + return mPermanentActivities.at(idx); + } catch (std::out_of_range &) { + return nullptr; + } +} + +std::shared_ptr PresencePerson::getNthPermanentActivitiesNote(unsigned int idx) const { + try { + return mPermanentActivitiesNotes.at(idx); + } catch (std::out_of_range &) { + return nullptr; + } +} + time_t PresencePerson::getTimestamp() const { return mTimestamp; } @@ -100,24 +127,49 @@ time_t PresencePerson::getTimestamp() const { // ----------------------------------------------------------------------------- LinphoneStatus PresencePerson::addActivity(const std::shared_ptr &activity) { - if (activity == nullptr) return -1; + if (activity == nullptr) { + return -1; + } // Insert in first position since its the most recent activity! mActivities.insert(mActivities.cbegin(), activity); return 0; } LinphoneStatus PresencePerson::addActivitiesNote(const std::shared_ptr ¬e) { - if (note == nullptr) return -1; + if (note == nullptr) { + return -1; + } mNotes.insert(mActivitiesNotes.cbegin(), note); return 0; } LinphoneStatus PresencePerson::addNote(const std::shared_ptr ¬e) { - if (note == nullptr) return -1; + if (note == nullptr) { + return -1; + } mNotes.insert(mNotes.cbegin(), note); return 0; } +LinphoneStatus PresencePerson::addPermanentActivity(const std::shared_ptr &activity) { + if (activity == nullptr) { + return -1; + } + // Insert in first position since its the most recent activity! + mPermanentActivities.insert(mPermanentActivities.cbegin(), activity); + mPermanentActivitiesToBePublished = true; + return 0; +} + +LinphoneStatus PresencePerson::addPermanentActivitiesNote(const std::shared_ptr ¬e) { + if (note == nullptr) { + return -1; + } + mPermanentActivitiesNotes.insert(mPermanentActivitiesNotes.cbegin(), note); + mPermanentActivitiesToBePublished = true; + return 0; +} + void PresencePerson::clearActivities() { mActivities.clear(); } @@ -130,6 +182,20 @@ void PresencePerson::clearNotes() { mNotes.clear(); } +void PresencePerson::clearPermanentActivities() { + if (!mPermanentActivities.empty()) { + mPermanentActivities.clear(); + mPermanentActivitiesToBePublished = true; + } +} + +void PresencePerson::clearPermanentActivitiesNotes() { + if (!mPermanentActivitiesNotes.empty()) { + mPermanentActivitiesNotes.clear(); + mPermanentActivitiesToBePublished = true; + } +} + bool PresencePerson::hasActivities() const { return !mActivities.empty(); } @@ -142,6 +208,14 @@ bool PresencePerson::hasNotes() const { return !mNotes.empty(); } +bool PresencePerson::hasPermanentActivities() const { + return !mPermanentActivities.empty(); +} + +bool PresencePerson::hasPermanentActivitiesNotes() const { + return !mPermanentActivitiesNotes.empty(); +} + // ----------------------------------------------------------------------------- #ifdef HAVE_XML2 @@ -150,36 +224,47 @@ int PresencePerson::parsePidfXmlPresenceActivities(XmlParsingContext &xmlContext int err = 0; ss << PresencePerson::pidfXmlPrefix.data() << "[" << personIdx << "]/rpid:activities"; xmlXPathObjectPtr activitiesNodesObject = xmlContext.getXpathObjectForNodeList(ss.str()); - if (activitiesNodesObject && activitiesNodesObject->nodesetval) { + if ((activitiesNodesObject != nullptr) && (activitiesNodesObject->nodesetval != nullptr)) { for (int i = 1; i <= activitiesNodesObject->nodesetval->nodeNr; i++) { ss.clear(), ss.str(std::string()), ss << PresencePerson::pidfXmlPrefix.data() << "[" << personIdx << "]/rpid:activities[" << i << "]/rpid:*"; xmlXPathObjectPtr activitiesObject = xmlContext.getXpathObjectForNodeList(ss.str()); - if (activitiesObject && activitiesObject->nodesetval) { + if ((activitiesObject != nullptr) && (activitiesObject->nodesetval != nullptr)) { for (int j = 0; j < activitiesObject->nodesetval->nodeNr; j++) { xmlNodePtr activityNode = activitiesObject->nodesetval->nodeTab[j]; - if (activityNode->name && PresenceActivity::isValidActivityName((const char *)activityNode->name)) { + if ((activityNode->name != nullptr) && + PresenceActivity::isValidActivityName((const char *)activityNode->name)) { LinphonePresenceActivityType activityType; char *description = (char *)xmlNodeGetContent(activityNode); - if (description && (description[0] == '\0')) { + if ((description != nullptr) && (description[0] == '\0')) { xmlFree(description); description = nullptr; } err = PresenceActivity::activityNameToType((const char *)activityNode->name, &activityType); - if (err < 0) break; + if (err < 0) { + break; + } std::shared_ptr activity = PresenceActivity::create(activityType, L_C_TO_STRING(description)); addActivity(activity); - if (description) xmlFree(description); + if (description != nullptr) { + xmlFree(description); + } } } } - if (activitiesObject) xmlXPathFreeObject(activitiesObject); - if (err < 0) break; + if (activitiesObject != nullptr) { + xmlXPathFreeObject(activitiesObject); + } + if (err < 0) { + break; + } } } - if (activitiesNodesObject) xmlXPathFreeObject(activitiesNodesObject); + if (activitiesNodesObject != nullptr) { + xmlXPathFreeObject(activitiesNodesObject); + } return err; } @@ -188,13 +273,15 @@ int PresencePerson::parsePidfXmlPresenceNotes(XmlParsingContext &xmlContext, uns stringstream ss; ss << PresencePerson::pidfXmlPrefix.data() << "[" << personIdx << "]/rpid:activities/rpid:note"; xmlXPathObjectPtr noteObject = xmlContext.getXpathObjectForNodeList(ss.str()); - if (noteObject && noteObject->nodesetval) { + if ((noteObject != nullptr) && (noteObject->nodesetval != nullptr)) { for (int i = 1; i <= noteObject->nodesetval->nodeNr; i++) { ss.clear(), ss.str(std::string()), ss << PresencePerson::pidfXmlPrefix.data() << "[" << personIdx << "]/rpid:activities/rpid:note[" << i << "]"; std::string noteStr = xmlContext.getTextContent(ss.str()); - if (noteStr.empty()) continue; + if (noteStr.empty()) { + continue; + } ss.clear(), ss.str(std::string()), ss << PresencePerson::pidfXmlPrefix.data() << "[" << personIdx << "]/rpid:activities/rpid:note[" << i << "]/@xml:lang"; @@ -203,15 +290,42 @@ int PresencePerson::parsePidfXmlPresenceNotes(XmlParsingContext &xmlContext, uns addActivitiesNote(note); } } - if (noteObject) xmlXPathFreeObject(noteObject); + if (noteObject != nullptr) { + xmlXPathFreeObject(noteObject); + } + ss.clear(), ss.str(std::string()), + ss << PresencePerson::pidfXmlPrefix.data() << "[" << personIdx << "]/rpid:permanent-activities/rpid:note"; + noteObject = xmlContext.getXpathObjectForNodeList(ss.str()); + if ((noteObject != nullptr) && (noteObject->nodesetval != nullptr)) { + for (int i = 1; i <= noteObject->nodesetval->nodeNr; i++) { + ss.clear(), ss.str(std::string()), + ss << PresencePerson::pidfXmlPrefix.data() << "[" << personIdx + << "]/rpid:permanent-activities/rpid:note[" << i << "]"; + std::string noteStr = xmlContext.getTextContent(ss.str()); + if (noteStr.empty()) { + continue; + } + ss.clear(), ss.str(std::string()), + ss << PresencePerson::pidfXmlPrefix.data() << "[" << personIdx + << "]/rpid:permanent-activities/rpid:note[" << i << "]/@xml:lang"; + std::string lang = xmlContext.getTextContent(ss.str()); + std::shared_ptr note = PresenceNote::create(noteStr, lang); + addPermanentActivitiesNote(note); + } + } + if (noteObject != nullptr) { + xmlXPathFreeObject(noteObject); + } ss.clear(), ss.str(std::string()), ss << PresencePerson::pidfXmlPrefix.data() << "[" << personIdx << "]/dm:note"; noteObject = xmlContext.getXpathObjectForNodeList(ss.str()); - if (noteObject && noteObject->nodesetval) { + if ((noteObject != nullptr) && (noteObject->nodesetval != nullptr)) { for (int i = 1; i <= noteObject->nodesetval->nodeNr; i++) { ss.clear(), ss.str(std::string()), ss << PresencePerson::pidfXmlPrefix.data() << "[" << personIdx << "]/dm:note[" << i << "]"; std::string noteStr = xmlContext.getTextContent(ss.str()); - if (noteStr.empty()) continue; + if (noteStr.empty()) { + continue; + } ss.clear(), ss.str(std::string()), ss << PresencePerson::pidfXmlPrefix.data() << "[" << personIdx << "]/dm:note[" << i << "]/@xml:lang"; std::string lang = xmlContext.getTextContent(ss.str()); @@ -219,27 +333,85 @@ int PresencePerson::parsePidfXmlPresenceNotes(XmlParsingContext &xmlContext, uns addNote(note); } } - if (noteObject) xmlXPathFreeObject(noteObject); + if (noteObject != nullptr) { + xmlXPathFreeObject(noteObject); + } return 0; } +int PresencePerson::parsePidfXmlPresencePermanentActivities(XmlParsingContext &xmlContext, unsigned int personIdx) { + stringstream ss; + int err = 0; + ss << PresencePerson::pidfXmlPrefix.data() << "[" << personIdx << "]/rpid:permanent-activities"; + xmlXPathObjectPtr permanentActivitiesNodesObject = xmlContext.getXpathObjectForNodeList(ss.str()); + if ((permanentActivitiesNodesObject != nullptr) && (permanentActivitiesNodesObject->nodesetval != nullptr)) { + for (int i = 1; i <= permanentActivitiesNodesObject->nodesetval->nodeNr; i++) { + ss.clear(), ss.str(std::string()), + ss << PresencePerson::pidfXmlPrefix.data() << "[" << personIdx << "]/rpid:permanent-activities[" << i + << "]/rpid:*"; + xmlXPathObjectPtr permanentActivitiesObject = xmlContext.getXpathObjectForNodeList(ss.str()); + if ((permanentActivitiesObject != nullptr) && (permanentActivitiesObject->nodesetval != nullptr)) { + for (int j = 0; j < permanentActivitiesObject->nodesetval->nodeNr; j++) { + xmlNodePtr activityNode = permanentActivitiesObject->nodesetval->nodeTab[j]; + if ((activityNode->name != nullptr) && + PresenceActivity::isValidActivityName((const char *)activityNode->name)) { + LinphonePresenceActivityType activityType; + char *description = (char *)xmlNodeGetContent(activityNode); + if ((description != nullptr) && (description[0] == '\0')) { + xmlFree(description); + description = nullptr; + } + err = PresenceActivity::activityNameToType((const char *)activityNode->name, &activityType); + if (err < 0) { + break; + } + std::shared_ptr activity = + PresenceActivity::create(activityType, L_C_TO_STRING(description)); + addPermanentActivity(activity); + if (description != nullptr) { + xmlFree(description); + } + } + } + } + if (permanentActivitiesObject != nullptr) { + xmlXPathFreeObject(permanentActivitiesObject); + } + if (err < 0) { + break; + } + } + } + if (permanentActivitiesNodesObject != nullptr) { + xmlXPathFreeObject(permanentActivitiesNodesObject); + } + + return err; +} + int PresencePerson::toXml(xmlTextWriterPtr writer) const { int err = xmlTextWriterStartElementNS(writer, (const xmlChar *)"dm", (const xmlChar *)"person", nullptr); if (err >= 0) { std::string id = getId(); - if (id.empty()) id = PresenceModel::generatePresenceId(); + if (id.empty()) { + id = PresenceModel::generatePresenceId(); + } err = xmlTextWriterWriteAttribute(writer, (const xmlChar *)"id", (const xmlChar *)L_STRING_TO_C(id)); } if ((err >= 0) && (hasActivitiesNotes() || hasActivities())) { err = xmlTextWriterStartElementNS(writer, (const xmlChar *)"rpid", (const xmlChar *)"activities", nullptr); if ((err >= 0) && hasActivitiesNotes()) { for (const auto ¬e : mActivitiesNotes) { - if (err >= 0) err = note->toXml(writer, "rpid"); + if (err >= 0) { + err = note->toXml(writer, "rpid"); + } } } if ((err >= 0) && hasActivities()) { for (const auto &activity : mActivities) { - if (err >= 0) err = activity->toXml(writer); + if (err >= 0) { + err = activity->toXml(writer); + } } } if (err >= 0) { @@ -247,9 +419,33 @@ int PresencePerson::toXml(xmlTextWriterPtr writer) const { err = xmlTextWriterEndElement(writer); } } + if ((err >= 0) && mPermanentActivitiesToBePublished) { + err = xmlTextWriterStartElementNS(writer, (const xmlChar *)"rpid", (const xmlChar *)"permanent-activities", + nullptr); + if ((err >= 0) && hasPermanentActivitiesNotes()) { + for (const auto ¬e : mPermanentActivitiesNotes) { + if (err >= 0) { + err = note->toXml(writer, "rpid"); + } + } + } + if ((err >= 0) && hasPermanentActivities()) { + for (const auto &activity : mPermanentActivities) { + if (err >= 0) { + err = activity->toXml(writer); + } + } + } + if (err >= 0) { + /* Close the "permanent-activities" element. */ + err = xmlTextWriterEndElement(writer); + } + } if ((err >= 0) && hasNotes()) { for (const auto ¬e : mNotes) { - if (err >= 0) err = note->toXml(writer, "dm"); + if (err >= 0) { + err = note->toXml(writer, "dm"); + } } } if (err >= 0) { diff --git a/liblinphone/src/presence/presence-person.h b/liblinphone/src/presence/presence-person.h index 64bb6070dc..f1022251c9 100644 --- a/liblinphone/src/presence/presence-person.h +++ b/liblinphone/src/presence/presence-person.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2023 Belledonne Communications SARL. + * Copyright (c) 2010-2026 Belledonne Communications SARL. * * This file is part of Liblinphone * (see https://gitlab.linphone.org/BC/public/liblinphone). @@ -59,26 +59,37 @@ public: unsigned int getNbActivities() const; unsigned int getNbActivitiesNotes() const; unsigned int getNbNotes() const; - const std::shared_ptr getNthActivity(unsigned int idx) const; - const std::shared_ptr getNthActivitiesNote(unsigned int idx) const; - const std::shared_ptr getNthNote(unsigned int idx) const; + unsigned int getNbPermanentActivities() const; + unsigned int getNbPermanentActivitiesNotes() const; + std::shared_ptr getNthActivity(unsigned int idx) const; + std::shared_ptr getNthActivitiesNote(unsigned int idx) const; + std::shared_ptr getNthNote(unsigned int idx) const; + std::shared_ptr getNthPermanentActivity(unsigned int idx) const; + std::shared_ptr getNthPermanentActivitiesNote(unsigned int idx) const; time_t getTimestamp() const; // Other LinphoneStatus addActivity(const std::shared_ptr &activity); LinphoneStatus addActivitiesNote(const std::shared_ptr ¬e); LinphoneStatus addNote(const std::shared_ptr ¬e); + LinphoneStatus addPermanentActivity(const std::shared_ptr &activity); + LinphoneStatus addPermanentActivitiesNote(const std::shared_ptr ¬e); void clearActivities(); void clearActivitiesNotes(); void clearNotes(); + void clearPermanentActivities(); + void clearPermanentActivitiesNotes(); bool hasActivities() const; bool hasActivitiesNotes() const; bool hasNotes() const; + bool hasPermanentActivities() const; + bool hasPermanentActivitiesNotes() const; private: #ifdef HAVE_XML2 int parsePidfXmlPresenceActivities(XmlParsingContext &xmlContext, unsigned int personIdx); int parsePidfXmlPresenceNotes(XmlParsingContext &xmlContext, unsigned int personIdx); + int parsePidfXmlPresencePermanentActivities(XmlParsingContext &xmlContext, unsigned int personIdx); int toXml(xmlTextWriterPtr writer) const; static constexpr std::string_view pidfXmlPrefix = "/pidf:presence/dm:person"; @@ -89,6 +100,9 @@ private: std::vector> mActivities; std::vector> mActivitiesNotes; std::vector> mNotes; + std::vector> mPermanentActivities; + std::vector> mPermanentActivitiesNotes; + bool mPermanentActivitiesToBePublished = false; }; LINPHONE_END_NAMESPACE diff --git a/liblinphone/tester/CMakeLists.txt b/liblinphone/tester/CMakeLists.txt index 6be8a12dfb..5c97d29179 100644 --- a/liblinphone/tester/CMakeLists.txt +++ b/liblinphone/tester/CMakeLists.txt @@ -299,7 +299,6 @@ set(SOURCE_FILES_C log_file_tester.c message_tester.c player_tester.c - presence_server_tester.c presence_tester.c proxy_config_tester.c quality_reporting_tester.c @@ -363,6 +362,7 @@ set(SOURCE_FILES_CXX local-secure-chat-tester.cpp local-ephemeral-chat-tester.cpp potential_configuration_tester.cpp + presence_server_tester.cpp turn-server-tester.cpp vcard_tester.cpp vfs-encryption-tester.cpp diff --git a/liblinphone/tester/presence_server_tester.c b/liblinphone/tester/presence_server_tester.cpp similarity index 92% rename from liblinphone/tester/presence_server_tester.c rename to liblinphone/tester/presence_server_tester.cpp index 8511b02882..66a60b4fb1 100644 --- a/liblinphone/tester/presence_server_tester.c +++ b/liblinphone/tester/presence_server_tester.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2022 Belledonne Communications SARL. + * Copyright (c) 2010-2026 Belledonne Communications SARL. * * This file is part of Liblinphone * (see https://gitlab.linphone.org/BC/public/liblinphone). @@ -18,6 +18,9 @@ * along with this program. If not, see . */ +#include "bctoolbox/charconv.h" +#include "bctoolbox/tester.h" +#include "belle-sip/object.h" #include "liblinphone_tester.h" #include "linphone/api/c-account-params.h" #include "linphone/api/c-account.h" @@ -25,9 +28,12 @@ #include "linphone/api/c-dial-plan.h" #include "linphone/api/c-friend.h" #include "linphone/api/c-magic-search.h" +#include "linphone/api/c-presence.h" #include "linphone/api/c-search-result.h" #include "linphone/core.h" -#include "linphone/core_utils.h" +#include "linphone/types.h" +#include "presence/presence-model.h" +#include "private_functions.h" #include "tester_utils.h" // List coming from dialplan @@ -135,10 +141,10 @@ char *generate_random_e164_phone(void) { !check_phone_country(atoi(linphone_dial_plan_get_country_calling_code(dialPlan))) || (strcmp("52", linphone_dial_plan_get_country_calling_code(dialPlan)) == 0)) ; // fixme Linphone Mexican's dialplan has 2 ccc - belle_sip_object_remove_from_leak_detector((void *)genericDialPlan); + belle_sip_object_remove_from_leak_detector((belle_sip_object_t *)genericDialPlan); belle_sip_object_remove_from_leak_detector( - (void *)dialPlan); // because mostCommon dial plan is a static object freed at the end of the process. This f is - // only to avoid wrong leak detection. + (belle_sip_object_t *)dialPlan); // because mostCommon dial plan is a static object freed at the end of the + // process. This f is only to avoid wrong leak detection. return generate_random_e164_phone_from_dial_plan(dialPlan); } @@ -501,6 +507,7 @@ static void test_presence_list_base(bool_t enable_compression) { const char *pauline_identity; bctbx_list_t *lcs = NULL; LinphonePresenceModel *presence; + int previous_laure_number_of_NotifyPresenceReceived = 0; enable_publish_verified(marie, TRUE); enable_publish_verified(pauline, TRUE); @@ -634,7 +641,7 @@ static void test_presence_list_base(bool_t enable_compression) { linphone_core_set_presence_model(marie->lc, presence); linphone_presence_model_unref(presence); - int previous_laure_number_of_NotifyPresenceReceived = laure->stat.number_of_NotifyPresenceReceived; + previous_laure_number_of_NotifyPresenceReceived = laure->stat.number_of_NotifyPresenceReceived; wait_for_list(lcs, &laure->stat.number_of_NotifyPresenceReceived, previous_laure_number_of_NotifyPresenceReceived + 1, liblinphone_tester_sip_timeout); BC_ASSERT_EQUAL(laure->stat.number_of_NotifyPresenceReceived, previous_laure_number_of_NotifyPresenceReceived + 1, @@ -809,6 +816,8 @@ static void test_presence_list_same_friend_two_addresses(void) { LinphoneFriend *lf; const char *marie_identity; bctbx_list_t *lcs = NULL; + const LinphonePresenceModel *marie_presence_model = nullptr; + LinphoneConsolidatedPresence marie_presence = LinphoneConsolidatedPresenceOffline; enable_publish_verified(marie, TRUE); enable_publish_verified(pauline, TRUE); @@ -851,11 +860,9 @@ static void test_presence_list_same_friend_two_addresses(void) { BC_ASSERT_EQUAL(linphone_friend_get_consolidated_presence(lf), LinphoneConsolidatedPresenceOnline, int, "%d"); if (!BC_ASSERT_TRUE(linphone_friend_is_presence_received(lf))) goto end; - const LinphonePresenceModel *marie_presence_model = - linphone_friend_get_presence_model_for_uri_or_tel(lf, marie_identity); + marie_presence_model = linphone_friend_get_presence_model_for_uri_or_tel(lf, marie_identity); BC_ASSERT_PTR_NOT_NULL(marie_presence_model); - LinphoneConsolidatedPresence marie_presence = - linphone_presence_model_get_consolidated_presence(marie_presence_model); + marie_presence = linphone_presence_model_get_consolidated_presence(marie_presence_model); BC_ASSERT_EQUAL(marie_presence, LinphoneConsolidatedPresenceBusy, int, "%d"); end: @@ -1010,19 +1017,19 @@ static void test_presence_list_subscribe_with_error(bool_t io_error) { wait_for_until(laure->lc, pauline->lc, &laure->stat.number_of_LinphonePresenceActivityVacation, 2, 6000)); if (io_error) { ms_message("Simulating socket error"); - sal_set_recv_error(linphone_core_get_sal(laure->lc), -1); + linphone_core_get_sal(laure->lc)->setRecvError(-1); wait_for_list(lcs, &dummy, 1, 500); /* just time for socket to be closed */ } else { ms_message("Simulating in/out packets losses"); - sal_set_send_error(linphone_core_get_sal(laure->lc), - 1500); /*make sure no refresh is sent, trash the message without generating error*/ - sal_set_recv_error(linphone_core_get_sal(laure->lc), - 1500); /*make sure server notify to close the dialog is also ignored*/ + linphone_core_get_sal(laure->lc)->setSendError( + 1500); /*make sure no refresh is sent, trash the message without generating error*/ + linphone_core_get_sal(laure->lc)->setRecvError( + 1500); /*make sure server notify to close the dialog is also ignored*/ wait_for_list(lcs, &dummy, 1, 32000); /* Wait a little bit for the subscribe transaction to timeout */ } /*restart normal behavior*/ - sal_set_send_error(linphone_core_get_sal(laure->lc), 0); - sal_set_recv_error(linphone_core_get_sal(laure->lc), 1); + linphone_core_get_sal(laure->lc)->setSendError(0); + linphone_core_get_sal(laure->lc)->setRecvError(1); /*a new subscribe should be sent */ BC_ASSERT_TRUE( @@ -1436,9 +1443,9 @@ static void long_term_presence_with_phone_without_sip(void) { linphone_friend_unref(friend2); belle_sip_object_remove_from_leak_detector( - (void *)dialPlan); // because mostCommon dial plan is a static object freed at the end of the process. This - // f is only to avoid wrong leak detection. - belle_sip_object_remove_from_leak_detector((void *)genericDialPlan); + (belle_sip_object_t *)dialPlan); // because mostCommon dial plan is a static object freed at the end of the + // process. This f is only to avoid wrong leak detection. + belle_sip_object_remove_from_leak_detector((belle_sip_object_t *)genericDialPlan); linphone_core_manager_destroy(pauline); bctbx_free(e164); bctbx_free(phone); @@ -2426,8 +2433,8 @@ static void notify_friend_capabilities_after_publish(void) { bctbx_list_t *lcs = NULL; bctbx_list_t *specs = NULL; - specs = bctbx_list_append(specs, "groupchat/1.1"); - specs = bctbx_list_append(specs, "lime/1.5"); + specs = bctbx_list_append(specs, (char *)"groupchat/1.1"); + specs = bctbx_list_append(specs, (char *)"lime/1.5"); linphone_core_set_linphone_specs_list(pauline->lc, specs); bctbx_list_free(specs); @@ -2559,8 +2566,8 @@ static void notify_friend_capabilities_with_alias(void) { marie = linphone_core_manager_create2("marie_rc", e164Marie); linphone_config_set_bool(linphone_core_get_config(marie->lc), "lime", "enabled", FALSE); linphone_core_set_user_agent(marie->lc, "full-presence-support-bypass", NULL); - specs = bctbx_list_append(specs, "groupchat/1.1"); - specs = bctbx_list_append(specs, "lime/1.5"); + specs = bctbx_list_append(specs, (char *)"groupchat/1.1"); + specs = bctbx_list_append(specs, (char *)"lime/1.5"); linphone_core_set_linphone_specs_list(marie->lc, specs); bctbx_list_free(specs); linphone_core_manager_start(marie, TRUE); @@ -2613,9 +2620,9 @@ static void notify_friend_capabilities_with_alias(void) { linphone_friend_unref(marieFriend); linphone_friend_unref(laureFriend); belle_sip_object_remove_from_leak_detector( - (void *)dialPlan); // because mostCommon dial plan is a static object freed at the end of the process. - // This f is only to avoid wrong leak detection. - belle_sip_object_remove_from_leak_detector((void *)genericDialPlan); + (belle_sip_object_t *)dialPlan); // because mostCommon dial plan is a static object freed at the end of the + // process. This f is only to avoid wrong leak detection. + belle_sip_object_remove_from_leak_detector((belle_sip_object_t *)genericDialPlan); linphone_core_manager_destroy(pauline); bctbx_free(e164Laure); bctbx_free(phoneLaure); @@ -2648,8 +2655,8 @@ static void notify_search_result_capabilities_with_alias(void) { marie = linphone_core_manager_create2("marie_rc", e164Marie); linphone_core_set_user_agent(marie->lc, "full-presence-support-bypass", NULL); - specs = bctbx_list_append(specs, "groupchat/1.1"); - specs = bctbx_list_append(specs, "lime/1.5"); + specs = bctbx_list_append(specs, (char *)"groupchat/1.1"); + specs = bctbx_list_append(specs, (char *)"lime/1.5"); linphone_core_set_linphone_specs_list(marie->lc, specs); bctbx_list_free(specs); linphone_core_manager_start(marie, TRUE); @@ -2679,7 +2686,7 @@ static void notify_search_result_capabilities_with_alias(void) { if (BC_ASSERT_PTR_NOT_NULL(resultList)) { BC_ASSERT_EQUAL((int)bctbx_list_size(resultList), 1, int, "%d"); - const LinphoneSearchResult *sr = bctbx_list_nth_data(resultList, 0); + const auto *sr = reinterpret_cast(bctbx_list_nth_data(resultList, 0)); BC_ASSERT_TRUE(linphone_search_result_has_capability(sr, LinphoneFriendCapabilityGroupChat)); BC_ASSERT_TRUE(linphone_search_result_has_capability(sr, LinphoneFriendCapabilityLimeX3dh)); @@ -2699,9 +2706,9 @@ static void notify_search_result_capabilities_with_alias(void) { linphone_friend_unref(marieFriend); belle_sip_object_remove_from_leak_detector( - (void *)dialPlan); // because mostCommon dial plan is a static object freed at the end of the process. - // This f is only to avoid wrong leak detection. - belle_sip_object_remove_from_leak_detector((void *)genericDialPlan); + (belle_sip_object_t *)dialPlan); // because mostCommon dial plan is a static object freed at the end of the + // process. This f is only to avoid wrong leak detection. + belle_sip_object_remove_from_leak_detector((belle_sip_object_t *)genericDialPlan); linphone_core_manager_destroy(pauline); bctbx_free(e164Marie); @@ -2713,6 +2720,181 @@ static void notify_search_result_capabilities_with_alias(void) { } #endif +static void enable_echoed_presence_subscription(LinphoneCoreManager *mgr) { + LinphoneAccountParams *account_params = + linphone_account_params_clone(linphone_account_get_params(linphone_core_get_default_account(mgr->lc))); + linphone_account_params_enable_echoed_presence_subscription(account_params, TRUE); + linphone_account_set_params(linphone_core_get_default_account(mgr->lc), account_params); + linphone_account_params_unref(account_params); +} + +/* + * It is not possible to test a whole path of permanent activities since flexisip-presence does not handle it. + * Test only the SUBSCRIBE path here, by checking that we get a NOTIFY for the local identity when the echoed presence + * subscription is enabled. That validates that the local identity has correctly been added to the SUBSCRIBE. + * Also check that we have an echoed presence model after receiving the NOTIFY. + */ +void permanent_activities_subscribe() { + LinphoneCoreManager *laure = linphone_core_presence_manager_new("laure_tcp_rc"); + LinphoneCoreManager *pauline = linphone_core_presence_manager_new( + transport_supported(LinphoneTransportTls) == TRUE ? "pauline_rc" : "pauline_tcp_rc"); + linphone_core_set_user_agent(laure->lc, "bypass", nullptr); + linphone_core_set_user_agent(pauline->lc, "bypass", nullptr); + + enable_echoed_presence_subscription(laure); + enable_publish_verified(laure, TRUE); + enable_publish_verified(pauline, TRUE); + + const char *rls_uri = "sip:rls@sip.example.org"; + const char *pauline_identity = get_identity(pauline); + bctbx_list_t *lcs = nullptr; + lcs = bctbx_list_append(lcs, laure->lc); + lcs = bctbx_list_append(lcs, pauline->lc); + + LinphoneFriendList *lfl = linphone_core_create_friend_list(laure->lc); + linphone_friend_list_set_rls_uri(lfl, rls_uri); + LinphoneFriend *lf = linphone_core_create_friend_with_address(laure->lc, pauline_identity); + linphone_friend_list_add_friend(lfl, lf); + linphone_friend_unref(lf); + linphone_core_add_friend_list(laure->lc, lfl); + linphone_friend_list_update_subscriptions(lfl); + linphone_friend_list_unref(lfl); + + wait_for_list(lcs, &laure->stat.number_of_NotifyPresenceReceived, 2, liblinphone_tester_sip_timeout); + + BC_ASSERT_PTR_NOT_NULL(linphone_account_get_echoed_presence_model(linphone_core_get_default_account(laure->lc))); + + bctbx_list_free(lcs); + linphone_core_manager_destroy(pauline); + linphone_core_manager_destroy(laure); +} + +/* + * It is not possible to test a whole path of permanent activities since flexisip-presence does not handle it. + * Test only the PUBLISH path here, more precisely test the presence XML generation when adding and removing permanent + * activities. + */ +void permanent_activities_publish() { + LinphoneCoreManager *laure = linphone_core_manager_new("laure_tcp_rc"); + + LinphonePresenceModel *presence_model = + linphone_presence_model_new_with_activity(LinphonePresenceActivityDinner, nullptr); + linphone_presence_model_set_presentity( + presence_model, linphone_account_params_get_identity_address( + linphone_account_get_params(linphone_core_get_default_account(laure->lc)))); + + auto check_permanent_activities = [laure, presence_model](std::string lookup, bool expected_to_be_found) { + char *xml = linphone_presence_model_to_xml(presence_model); + if (expected_to_be_found) { + BC_ASSERT(std::string(xml).find(lookup) != std::string::npos); + } else { + BC_ASSERT(std::string(xml).find(lookup) == std::string::npos); + } + bctbx_free(xml), xml = nullptr; + linphone_account_set_presence_model(linphone_core_get_default_account(laure->lc), presence_model); + }; + + // Check that there is no permanent activities at the start. + check_permanent_activities("", false); + + // Check that the permanent activities are present in the XML when adding one. + auto *activity = linphone_presence_activity_new(LinphonePresenceActivityAway, nullptr); + linphone_presence_person_add_permanent_activity(linphone_presence_model_get_nth_person(presence_model, 0), + activity); + linphone_presence_activity_unref(activity); + check_permanent_activities("", true); + + // Check that modifying something other than the permanent activities in the model does not put the permanent + // activities section in the XML. + auto *person = linphone_presence_model_get_nth_person(presence_model, 0); + linphone_presence_person_clear_activities(person); + activity = linphone_presence_activity_new(LinphonePresenceActivityMeeting, nullptr); + linphone_presence_person_add_activity(person, activity); + linphone_presence_activity_unref(activity); + check_permanent_activities("", false); + + // Check that the permanent activities are present in the XML when adding another one. + activity = linphone_presence_activity_new(LinphonePresenceActivityInTransit, nullptr); + linphone_presence_person_add_permanent_activity(linphone_presence_model_get_nth_person(presence_model, 0), + activity); + linphone_presence_activity_unref(activity); + check_permanent_activities("", true); + + // Check that the permanent activities are present in the XML when adding a permanent activity note. + auto *note = linphone_presence_note_new("A simple note", "en"); + linphone_presence_person_add_permanent_activities_note(linphone_presence_model_get_nth_person(presence_model, 0), + note); + linphone_presence_note_unref(note); + check_permanent_activities("", true); + + // Check that the permanent activities section is still present in the XML when clearing the permanent activities, + // because the previously added permanent activity note is still there. + linphone_presence_person_clear_permanent_activities(linphone_presence_model_get_nth_person(presence_model, 0)); + check_permanent_activities("", true); + + // Check that the permanent activities section is present but empty in the XML when clearing the permanent + // activities notes. + linphone_presence_person_clear_permanent_activities_notes( + linphone_presence_model_get_nth_person(presence_model, 0)); + check_permanent_activities("", true); + + // Check one more time that modifying something other than the permanent activities in the model does not put the + // permanent activities section in the XML. + linphone_presence_person_clear_activities(linphone_presence_model_get_nth_person(presence_model, 0)); + check_permanent_activities("", false); + + linphone_presence_model_unref(presence_model); + + linphone_core_manager_destroy(laure); +} + +/* + * It is not possible to test a whole path of permanent activities since flexisip-presence does not handle it. + * Test only the NOTIFY path here, more precisely test the presence XML with permanent activities parsing. + */ +void permanent_activities_notify() { + auto *presence_model = reinterpret_cast(LinphonePrivate::PresenceModel::parsePresence( + "application", "pidf+xml", + "\n" + "\n" + " \n" + " \n" + " open\n" + " \n" + " sip:laure_agmtzpl@sip.example.org\n" + " 2026-07-09T14:21:48Z\n" + " \n" + " " + " \n" + " \n" + " \n" + " \n" + " A simple note\n" + " \n" + " \n" + " \n" + " 2026-07-09T14:21:48Z\n" + " \n" + "")); + BC_ASSERT_PTR_NOT_NULL(presence_model); + auto *person = linphone_presence_model_get_nth_person(presence_model, 0); + BC_ASSERT_PTR_NOT_NULL(person); + BC_ASSERT_EQUAL(linphone_presence_person_get_nb_permanent_activities(person), 2, int, "%d"); + auto *activity = linphone_presence_person_get_nth_permanent_activity(person, 0); + BC_ASSERT_PTR_NOT_NULL(activity); + BC_ASSERT_EQUAL(linphone_presence_activity_get_type(activity), LinphonePresenceActivityAway, int, "%d"); + activity = linphone_presence_person_get_nth_permanent_activity(person, 1); + BC_ASSERT_PTR_NOT_NULL(activity); + BC_ASSERT_EQUAL(linphone_presence_activity_get_type(activity), LinphonePresenceActivityInTransit, int, "%d"); + BC_ASSERT_EQUAL(linphone_presence_person_get_nb_permanent_activities_notes(person), 1, int, "%d"); + auto *note = linphone_presence_person_get_nth_permanent_activities_note(person, 0); + BC_ASSERT_PTR_NOT_NULL(note); + BC_ASSERT_STRING_EQUAL(linphone_presence_note_get_content(note), "A simple note"); + BC_ASSERT_STRING_EQUAL(linphone_presence_note_get_lang(note), "en"); + linphone_presence_model_unref(presence_model); +} + test_t presence_server_tests[] = { TEST_NO_TAG("Simple Publish", simple_publish), TEST_NO_TAG("Publish with 2 identities", publish_with_dual_identity), @@ -2767,11 +2949,14 @@ test_t presence_server_tests[] = { TEST_ONE_TAG( "Notify search result capabilities with alias", notify_search_result_capabilities_with_alias, "capabilities"), #endif + TEST_ONE_TAG("Permanent activities SUBSCRIBE", permanent_activities_subscribe, "permanent-activities"), + TEST_ONE_TAG("Permanent activities PUBLISH", permanent_activities_publish, "permanent-activities"), + TEST_ONE_TAG("Permanent activities NOTIFY", permanent_activities_notify, "permanent-activities"), }; test_suite_t presence_server_test_suite = {"Presence using server", - NULL, - NULL, + nullptr, + nullptr, liblinphone_tester_before_each, liblinphone_tester_after_each, sizeof(presence_server_tests) / sizeof(presence_server_tests[0]),