LCOV - code coverage report
Current view: top level - src/plugin - chatservicesmanager.cpp (source / functions) Coverage Total Hit
Test: jami-coverage-filtered.info Lines: 52.3 % 373 195
Test Date: 2026-07-06 08:25:38 Functions: 36.2 % 58 21

            Line data    Source code
       1              : /*
       2              :  *  Copyright (C) 2004-2026 Savoir-faire Linux Inc.
       3              :  *
       4              :  *  This program is free software: you can redistribute it and/or modify
       5              :  *  it under the terms of the GNU General Public License as published by
       6              :  *  the Free Software Foundation, either version 3 of the License, or
       7              :  *  (at your option) any later version.
       8              :  *
       9              :  *  This program is distributed in the hope that it will be useful,
      10              :  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
      11              :  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
      12              :  *  GNU General Public License for more details.
      13              :  *
      14              :  *  You should have received a copy of the GNU General Public License
      15              :  *  along with this program. If not, see <https://www.gnu.org/licenses/>.
      16              :  */
      17              : 
      18              : #include "chatservicesmanager.h"
      19              : #include "pluginmanager.h"
      20              : #include "streamdata.h"
      21              : #include "jamidht/commit_message.h"
      22              : #include "logger.h"
      23              : #include "manager.h"
      24              : #include "jamidht/jamiaccount.h"
      25              : #include "jamidht/conversation.h"
      26              : #include "fileutils.h"
      27              : #include "jami/conversation_interface.h"
      28              : 
      29              : namespace jami {
      30              : 
      31           33 : ChatServicesManager::ChatServicesManager(PluginManager& pluginManager)
      32              : {
      33           33 :     registerComponentsLifeCycleManagers(pluginManager);
      34           33 :     registerChatService(pluginManager);
      35           33 :     PluginPreferencesUtils::getAllowDenyListPreferences(allowDenyList_);
      36           33 : }
      37              : 
      38              : void
      39           33 : ChatServicesManager::registerComponentsLifeCycleManagers(PluginManager& pluginManager)
      40              : {
      41              :     // registerChatHandler may be called by the PluginManager upon loading a plugin.
      42            8 :     auto registerChatHandler = [this](void* data, std::mutex& pmMtx_) {
      43            8 :         std::lock_guard const lk(pmMtx_);
      44            8 :         ChatHandlerPtr ptr {(static_cast<ChatHandler*>(data))};
      45              : 
      46            8 :         if (!ptr) {
      47            0 :             return -1;
      48              :         }
      49           24 :         const auto handlerName = ptr->getChatHandlerDetails().at("name");
      50            8 :         auto* rawPtr = ptr.get();
      51            8 :         PluginPreferencesUtils::addAlwaysHandlerPreference(handlerName,
      52           24 :                                                            ptr->id().substr(0,
      53           16 :                                                                             ptr->id().find_last_of(DIR_SEPARATOR_CH)));
      54              : 
      55              :         // Snapshot under mtx_, notify outside: notifyChatSubject may call back into
      56              :         // ChatServicesManager and deadlock if mtx_ is still held.
      57              :         using NotifyItem = std::pair<std::pair<std::string, std::string>, chatSubjectPtr>;
      58            8 :         std::vector<NotifyItem> toNotify;
      59              :         {
      60            8 :             std::lock_guard const guard(mtx_);
      61            8 :             handlersNameMap_[handlerName] = reinterpret_cast<uintptr_t>(rawPtr);
      62            8 :             chatHandlers_.emplace_back(std::move(ptr));
      63              : 
      64              :             // Re-activate for conversations that had this handler enabled before (re)load.
      65            8 :             for (auto& [key, allowDenySet] : allowDenyList_) {
      66            0 :                 if (const auto it = allowDenySet.find(handlerName); it == allowDenySet.end() || !it->second) {
      67            0 :                     continue;
      68              :                 }
      69            0 :                 auto& subject = chatSubjects_.emplace(key, std::make_shared<PublishObservable<pluginMessagePtr>>())
      70            0 :                                     .first->second;
      71            0 :                 chatHandlerToggled_[key].insert(reinterpret_cast<uintptr_t>(rawPtr));
      72            0 :                 toNotify.emplace_back(key, subject);
      73              :             }
      74            8 :         } // release mtx_
      75              : 
      76            8 :         for (auto& [connection, subject] : toNotify) {
      77            0 :             JAMI_DEBUG("registerChatHandler: re-activating {} for {}/{}",
      78              :                        handlerName,
      79              :                        connection.first,
      80              :                        connection.second);
      81            0 :             rawPtr->notifyChatSubject(connection, subject);
      82              :         }
      83              : 
      84            8 :         return 0;
      85            8 :     };
      86              : 
      87              :     // unregisterChatHandler may be called by the PluginManager while unloading.
      88            8 :     auto unregisterChatHandler = [this](void* data, std::mutex& pmMtx_) {
      89            8 :         std::lock_guard const lk(pmMtx_);
      90              : 
      91              :         // Snapshot under mtx_, detach and destroy outside: destructor calls
      92              :         // getEnabledConversationsForPlugin which acquires mtx_ — would deadlock.
      93            8 :         ChatHandlerPtr handlerToDestroy;
      94              :         using DetachItem = std::pair<ChatHandler*, chatSubjectPtr>;
      95            8 :         std::vector<DetachItem> toDetach;
      96            8 :         std::vector<std::pair<std::string, std::string>> toClearOverwrites;
      97              :         {
      98            8 :             std::lock_guard const guard(mtx_);
      99            8 :             const auto handlerIt = std::ranges::find_if(chatHandlers_,
     100            8 :                                                         [data](const ChatHandlerPtr& h) { return h.get() == data; });
     101              : 
     102            8 :             if (handlerIt != chatHandlers_.end()) {
     103            9 :                 for (auto& [key, toggledSet] : chatHandlerToggled_) {
     104            1 :                     if (toggledSet.erase(reinterpret_cast<uintptr_t>(handlerIt->get()))) {
     105            0 :                         if (auto subjIt = chatSubjects_.find(key); subjIt != chatSubjects_.end()) {
     106            0 :                             toDetach.emplace_back(handlerIt->get(), subjIt->second);
     107              :                         }
     108            0 :                         toClearOverwrites.push_back(key);
     109              :                     }
     110              :                 }
     111           24 :                 handlersNameMap_.erase((*handlerIt)->getChatHandlerDetails().at("name"));
     112            8 :                 handlerToDestroy = std::move(*handlerIt);
     113            8 :                 chatHandlers_.erase(handlerIt);
     114              :             }
     115            8 :         } // release mtx_
     116              : 
     117            8 :         for (auto& [handler, subject] : toDetach) {
     118            0 :             handler->detach(subject);
     119              :         }
     120              : 
     121            8 :         for (const auto& [accountId, convId] : toClearOverwrites) {
     122            0 :             if (const auto acc = Manager::instance().getAccount<JamiAccount>(accountId)) {
     123            0 :                 if (auto* convMod = acc->convModule()) {
     124            0 :                     if (const auto conv = convMod->getConversation(convId)) {
     125            0 :                         conv->clearBodyOverwrites();
     126            0 :                     }
     127              :                 }
     128            0 :             }
     129              :         }
     130            8 :         handlerToDestroy.reset(); // destructor acquires mtx_ via getEnabledConversationsForPlugin
     131              : 
     132            8 :         return true;
     133            8 :     };
     134              : 
     135              :     // Services are registered to the PluginManager.
     136           99 :     pluginManager.registerComponentManager("ChatHandlerManager", registerChatHandler, unregisterChatHandler);
     137           33 : }
     138              : 
     139              : void
     140           33 : ChatServicesManager::registerChatService(PluginManager& pluginManager)
     141              : {
     142           99 :     pluginManager.registerService("sendTextMessage", [](const DLPlugin* p, void* d) {
     143            0 :         return sendTextMessage(p, static_cast<JamiMessage*>(d));
     144              :     });
     145           99 :     pluginManager.registerService("editMessage", [this](const DLPlugin* p, void* d) {
     146            0 :         return editMessage(p, static_cast<const JamiMessage*>(d));
     147              :     });
     148           99 :     pluginManager.registerService("reloadBodyOverwritesAllConversations", [this](const DLPlugin* p, void* d) {
     149            0 :         return reloadBodyOverwritesAllConversations(p, d);
     150              :     });
     151           99 :     pluginManager.registerService("updateMessageBodyOverwrite", [this](const DLPlugin* p, void* d) {
     152            0 :         return updateMessageBodyOverwrite(p, static_cast<const MessageBodyOverwriteUpdate*>(d));
     153              :     });
     154           99 :     pluginManager.registerService("clearBodyOverwritesAllConversations", [this](const DLPlugin* p, void* d) {
     155            0 :         return clearBodyOverwritesAllConversations(p, d);
     156              :     });
     157           99 :     pluginManager.registerService("loadMissingBodyOverwritesAllConversations", [this](const DLPlugin* p, void* d) {
     158            0 :         return loadMissingBodyOverwritesAllConversations(p, d);
     159              :     });
     160           99 :     pluginManager.registerService("clearBodyOverwrites", [this](const DLPlugin* p, void* d) {
     161            0 :         return clearBodyOverwrites(p, static_cast<const std::pair<std::string, std::string>*>(d));
     162              :     });
     163           99 :     pluginManager.registerService("loadMissingBodyOverwrites", [this](const DLPlugin* p, void* d) {
     164            0 :         return loadMissingBodyOverwrites(p, static_cast<const std::pair<std::string, std::string>*>(d));
     165              :     });
     166           33 : }
     167              : 
     168              : int
     169            0 : ChatServicesManager::sendTextMessage(const DLPlugin*, const JamiMessage* cm)
     170              : {
     171            0 :     if (!cm || cm->accountId.empty() || cm->peerId.empty()) {
     172            0 :         return -1;
     173              :     }
     174            0 :     if (const auto acc = jami::Manager::instance().getAccount<jami::JamiAccount>(cm->accountId)) {
     175              :         try {
     176            0 :             if (cm->isSwarm) {
     177            0 :                 acc->convModule()->sendMessage(cm->peerId, cm->data.at("body"));
     178              :             } else {
     179            0 :                 jami::Manager::instance().sendTextMessage(cm->accountId, cm->peerId, cm->data, true);
     180              :             }
     181            0 :         } catch (const std::exception& e) {
     182            0 :             JAMI_ERROR("Exception during text message sending: {}", e.what());
     183            0 :         }
     184            0 :     }
     185            0 :     return 0;
     186              : }
     187              : 
     188              : int
     189            0 : ChatServicesManager::reloadBodyOverwritesAllConversations(const DLPlugin* plugin, void*)
     190              : {
     191            0 :     for (const auto& [accountId, convId] : getEnabledConversationsForPlugin(plugin)) {
     192            0 :         if (const auto acc = Manager::instance().getAccount<JamiAccount>(accountId)) {
     193            0 :             if (auto* convMod = acc->convModule()) {
     194            0 :                 if (const auto conv = convMod->getConversation(convId)) {
     195            0 :                     conv->reloadBodyOverwriteMessages();
     196            0 :                 }
     197              :             }
     198            0 :         }
     199            0 :     }
     200            0 :     return 0;
     201              : }
     202              : 
     203              : int
     204            0 : ChatServicesManager::updateMessageBodyOverwrite(const DLPlugin* plugin, const MessageBodyOverwriteUpdate* upd)
     205              : {
     206            0 :     if (!upd || upd->accountId.empty() || upd->conversationId.empty()) {
     207            0 :         return -1;
     208              :     }
     209            0 :     if (const auto enabled = getEnabledConversationsForPlugin(plugin);
     210            0 :         !enabled.contains({upd->accountId, upd->conversationId})) {
     211            0 :         JAMI_WARNING("updateMessageBodyOverwrite: plugin not enabled for {}/{}, rejecting",
     212              :                      upd->accountId,
     213              :                      upd->conversationId);
     214            0 :         return -1;
     215            0 :     }
     216            0 :     JAMI_DEBUG("updateMessageBodyOverwrite: plugin authorized for {}/{}", upd->accountId, upd->conversationId);
     217            0 :     if (const auto acc = Manager::instance().getAccount<JamiAccount>(upd->accountId)) {
     218            0 :         if (auto* convMod = acc->convModule()) {
     219            0 :             if (const auto conv = convMod->getConversation(upd->conversationId)) {
     220            0 :                 conv->updateMessageBodyOverwrite(upd->messageId, upd->bodyOverwrite);
     221            0 :             }
     222              :         }
     223            0 :     }
     224            0 :     return 0;
     225              : }
     226              : 
     227              : int
     228            0 : ChatServicesManager::clearBodyOverwritesAllConversations(const DLPlugin* plugin, void*)
     229              : {
     230            0 :     for (const auto& [accountId, convId] : getEnabledConversationsForPlugin(plugin)) {
     231            0 :         if (const auto acc = Manager::instance().getAccount<JamiAccount>(accountId)) {
     232            0 :             if (auto* convMod = acc->convModule()) {
     233            0 :                 if (const auto conv = convMod->getConversation(convId)) {
     234            0 :                     conv->clearBodyOverwrites();
     235            0 :                 }
     236              :             }
     237            0 :         }
     238            0 :     }
     239            0 :     return 0;
     240              : }
     241              : 
     242              : int
     243            0 : ChatServicesManager::loadMissingBodyOverwritesAllConversations(const DLPlugin* plugin, void*)
     244              : {
     245            0 :     for (const auto& [accountId, convId] : getEnabledConversationsForPlugin(plugin)) {
     246            0 :         if (const auto acc = Manager::instance().getAccount<JamiAccount>(accountId)) {
     247            0 :             if (auto* convMod = acc->convModule()) {
     248            0 :                 if (const auto conv = convMod->getConversation(convId)) {
     249            0 :                     conv->loadMissingBodyOverwrites();
     250            0 :                 }
     251              :             }
     252            0 :         }
     253            0 :     }
     254            0 :     return 0;
     255              : }
     256              : 
     257              : int
     258            0 : ChatServicesManager::editMessage(const DLPlugin* plugin, const JamiMessage* cm)
     259              : {
     260            0 :     if (!cm || cm->accountId.empty() || cm->peerId.empty()) {
     261            0 :         return -1;
     262              :     }
     263            0 :     if (const auto enabled = getEnabledConversationsForPlugin(plugin); !enabled.contains({cm->accountId, cm->peerId})) {
     264            0 :         JAMI_WARNING("editMessage: plugin not enabled for {}/{}, rejecting", cm->accountId, cm->peerId);
     265            0 :         return -1;
     266            0 :     }
     267            0 :     JAMI_DEBUG("editMessage: plugin authorized for {}/{}", cm->accountId, cm->peerId);
     268            0 :     if (const auto acc = jami::Manager::instance().getAccount<jami::JamiAccount>(cm->accountId)) {
     269              :         try {
     270            0 :             const auto bodyIt = cm->data.find(CommitKey::BODY);
     271            0 :             const auto editIt = cm->data.find(CommitKey::EDIT);
     272            0 :             if (bodyIt != cm->data.end() && editIt != cm->data.end()) {
     273            0 :                 acc->convModule()->editMessage(cm->peerId, bodyIt->second, editIt->second);
     274              :             }
     275            0 :         } catch (const std::exception& e) {
     276            0 :             JAMI_ERROR("Exception during plugin editMessage: {}", e.what());
     277            0 :         }
     278            0 :     }
     279            0 :     return 0;
     280              : }
     281              : 
     282              : int
     283            0 : ChatServicesManager::clearBodyOverwrites(const DLPlugin* plugin, const std::pair<std::string, std::string>* p)
     284              : {
     285            0 :     if (!p) {
     286            0 :         return 0;
     287              :     }
     288            0 :     if (const auto enabled = getEnabledConversationsForPlugin(plugin); !enabled.contains({p->first, p->second})) {
     289            0 :         return -1;
     290            0 :     }
     291            0 :     if (const auto acc = Manager::instance().getAccount<JamiAccount>(p->first)) {
     292            0 :         if (auto* convMod = acc->convModule()) {
     293            0 :             if (const auto conv = convMod->getConversation(p->second)) {
     294            0 :                 conv->clearBodyOverwrites();
     295            0 :             }
     296              :         }
     297            0 :     }
     298            0 :     return 0;
     299              : }
     300              : 
     301              : int
     302            0 : ChatServicesManager::loadMissingBodyOverwrites(const DLPlugin* plugin, const std::pair<std::string, std::string>* p)
     303              : {
     304            0 :     if (!p) {
     305            0 :         return 0;
     306              :     }
     307            0 :     if (const auto enabled = getEnabledConversationsForPlugin(plugin); !enabled.contains({p->first, p->second})) {
     308            0 :         return -1;
     309            0 :     }
     310            0 :     if (const auto acc = Manager::instance().getAccount<JamiAccount>(p->first)) {
     311            0 :         if (auto* convMod = acc->convModule()) {
     312            0 :             if (const auto conv = convMod->getConversation(p->second)) {
     313            0 :                 conv->loadMissingBodyOverwrites();
     314            0 :             }
     315              :         }
     316            0 :     }
     317            0 :     return 0;
     318              : }
     319              : 
     320              : void
     321            2 : ChatServicesManager::transformSwarmMessages(std::vector<libjami::SwarmMessage>& messages,
     322              :                                             const std::string& accountId,
     323              :                                             const std::string& conversationId)
     324              : {
     325              :     // Snapshot active handlers under mtx_, then call outside: a plugin's
     326              :     // transformSwarmMessages may invoke a service (e.g., clearBodyOverwritesAllConversations)
     327              :     // which calls getEnabledConversationsForPlugin → mtx_ → deadlock.
     328            2 :     std::vector<ChatHandlerPtr> toCall;
     329              :     {
     330            2 :         std::lock_guard const lk(mtx_);
     331            2 :         std::pair<std::string, std::string> const key(accountId, conversationId);
     332            2 :         const auto toggledIt = chatHandlerToggled_.find(key);
     333            2 :         const auto allowIt = allowDenyList_.find(key);
     334            4 :         for (auto& handler : chatHandlers_) {
     335            6 :             const auto handlerName = handler->getChatHandlerDetails().at("name");
     336            2 :             const bool toggled = toggledIt != chatHandlerToggled_.end()
     337            2 :                                  && toggledIt->second.contains(reinterpret_cast<uintptr_t>(handler.get()));
     338            2 :             bool allowed = false;
     339            2 :             if (allowIt != allowDenyList_.end()) {
     340            2 :                 if (const auto it = allowIt->second.find(handlerName); it != allowIt->second.end()) {
     341            2 :                     allowed = it->second;
     342              :                 }
     343              :             }
     344            2 :             if (toggled || allowed) {
     345            1 :                 toCall.push_back(handler);
     346              :             }
     347            2 :         }
     348            2 :     } // release mtx_
     349              : 
     350            2 :     for (const auto& handler : toCall) {
     351            1 :         handler->transformSwarmMessages(messages, accountId, conversationId);
     352              :     }
     353            2 : }
     354              : 
     355              : bool
     356         1229 : ChatServicesManager::hasHandlers() const
     357              : {
     358         1229 :     std::lock_guard const lk(mtx_);
     359         2458 :     return !chatHandlers_.empty();
     360         1229 : }
     361              : 
     362              : std::vector<std::string>
     363            2 : ChatServicesManager::getChatHandlers() const
     364              : {
     365            2 :     std::lock_guard const lk(mtx_);
     366            2 :     std::vector<std::string> res;
     367            2 :     res.reserve(chatHandlers_.size());
     368            4 :     for (const auto& chatHandler : chatHandlers_) {
     369            2 :         res.emplace_back(std::to_string(reinterpret_cast<uintptr_t>(chatHandler.get())));
     370              :     }
     371            4 :     return res;
     372            2 : }
     373              : 
     374              : void
     375            3 : ChatServicesManager::publishMessage(const pluginMessagePtr& message)
     376              : {
     377            3 :     if (message->fromPlugin) {
     378            0 :         return;
     379              :     }
     380              : 
     381            3 :     std::pair<std::string, std::string> mPair(message->accountId, message->peerId);
     382              : 
     383              :     // Collect activation work and the subject to publish under the lock, then
     384              :     // call notifyChatSubject and publish outside it. notifyChatSubject can block
     385              :     // (e.g. a handler that initialises a model synchronously) and may call back
     386              :     // into ChatServicesManager, both of which would deadlock if mtx_ is held.
     387              :     using ActivateItem = std::pair<ChatHandlerPtr, chatSubjectPtr>;
     388            3 :     std::vector<ActivateItem> toActivate;
     389            3 :     chatSubjectPtr subjectToPublish;
     390              : 
     391              :     {
     392            3 :         std::lock_guard const lk(mtx_);
     393            3 :         if (chatHandlers_.empty()) {
     394            0 :             return;
     395              :         }
     396              : 
     397            3 :         auto& handlers = chatHandlerToggled_[mPair];
     398            3 :         auto& chatAllowDenySet = allowDenyList_[mPair];
     399              : 
     400              :         // All handlers share one subject per {accountId, peerId} — publish once outside
     401              :         // the loop to avoid duplicate delivery when multiple handlers are active.
     402            3 :         bool shouldPublish = false;
     403            6 :         for (auto& chatHandler : chatHandlers_) {
     404            9 :             const auto chatHandlerName = chatHandler->getChatHandlerDetails().at("name");
     405            3 :             bool toggle = PluginPreferencesUtils::getAlwaysPreference(
     406            6 :                 chatHandler->id().substr(0, chatHandler->id().find_last_of(DIR_SEPARATOR_CH)),
     407              :                 chatHandlerName,
     408            3 :                 message->accountId);
     409            3 :             if (const auto allowedIt = chatAllowDenySet.find(chatHandlerName); allowedIt != chatAllowDenySet.end()) {
     410            2 :                 toggle = allowedIt->second;
     411              :             }
     412            3 :             const bool toggled = handlers.contains(reinterpret_cast<uintptr_t>(chatHandler.get()));
     413            3 :             if (toggle || toggled) {
     414            2 :                 auto& subject = chatSubjects_.emplace(mPair, std::make_shared<PublishObservable<pluginMessagePtr>>())
     415            2 :                                     .first->second;
     416            2 :                 if (!toggled) {
     417            0 :                     JAMI_DEBUG("publishMessage: auto-activating handler {} for {}/{}",
     418              :                                chatHandlerName,
     419              :                                mPair.first,
     420              :                                mPair.second);
     421            0 :                     handlers.insert(reinterpret_cast<uintptr_t>(chatHandler.get()));
     422            0 :                     chatAllowDenySet[chatHandlerName] = true;
     423            0 :                     toActivate.emplace_back(chatHandler, subject);
     424              :                 }
     425            2 :                 shouldPublish = true;
     426              :             }
     427            3 :         }
     428              : 
     429            3 :         if (shouldPublish) {
     430            2 :             if (const auto subjectIt = chatSubjects_.find(mPair); subjectIt != chatSubjects_.end()) {
     431            2 :                 subjectToPublish = subjectIt->second;
     432              :             }
     433              :         }
     434            3 :     } // release mtx_
     435              : 
     436              :     // Activate new handlers outside the lock (notifyChatSubject may call back into us).
     437            3 :     if (!toActivate.empty()) {
     438            0 :         for (auto& [handler, subject] : toActivate) {
     439            0 :             handler->notifyChatSubject(mPair, subject);
     440              :         }
     441            0 :         std::lock_guard const lk(mtx_);
     442            0 :         PluginPreferencesUtils::setAllowDenyListPreferences(allowDenyList_);
     443            0 :     }
     444              : 
     445              :     // Publish outside the lock: plugin callbacks (editMessage → publishMessage) would deadlock.
     446            3 :     if (subjectToPublish) {
     447            2 :         subjectToPublish->publish(message);
     448              :     }
     449            3 : }
     450              : 
     451              : void
     452          852 : ChatServicesManager::cleanChatSubjects(const std::string& accountId, const std::string& peerId)
     453              : {
     454         5074 :     JAMI_DEBUG("cleanChatSubjects: removing subjects for account={} peer={}",
     455              :                accountId,
     456              :                peerId.empty() ? "(all)" : peerId);
     457          852 :     std::lock_guard const lk(mtx_);
     458          852 :     const std::pair<std::string, std::string> mPair(accountId, peerId);
     459          853 :     for (auto it = chatSubjects_.begin(); it != chatSubjects_.end();) {
     460            1 :         if ((peerId.empty() && it->first.first == accountId) || (!peerId.empty() && it->first == mPair)) {
     461            1 :             it = chatSubjects_.erase(it);
     462              :         } else {
     463            0 :             ++it;
     464              :         }
     465              :     }
     466          852 : }
     467              : 
     468              : void
     469            2 : ChatServicesManager::toggleChatHandler(const std::string& chatHandlerId,
     470              :                                        const std::string& accountId,
     471              :                                        const std::string& peerId,
     472              :                                        const bool toggle)
     473              : {
     474            2 :     toggleChatHandler(std::stoull(chatHandlerId), accountId, peerId, toggle);
     475            2 : }
     476              : 
     477              : std::vector<std::string>
     478            2 : ChatServicesManager::getChatHandlerStatus(const std::string& accountId, const std::string& peerId)
     479              : {
     480            2 :     std::lock_guard const lk(mtx_);
     481            2 :     const std::pair<std::string, std::string> mPair(accountId, peerId);
     482            2 :     const auto it = allowDenyList_.find(mPair);
     483            2 :     std::vector<std::string> ret;
     484            2 :     if (it != allowDenyList_.end()) {
     485            4 :         for (const auto& [name, enabled] : it->second) {
     486            2 :             if (enabled && handlersNameMap_.contains(name)) {
     487            1 :                 ret.emplace_back(std::to_string(handlersNameMap_.at(name)));
     488              :             }
     489              :         }
     490              :     }
     491            4 :     return ret;
     492            2 : }
     493              : 
     494              : std::map<std::string, std::string>
     495            2 : ChatServicesManager::getChatHandlerDetails(const std::string& chatHandlerIdStr)
     496              : {
     497            2 :     std::lock_guard const lk(mtx_);
     498            2 :     const auto chatHandlerId = std::stoull(chatHandlerIdStr);
     499            2 :     for (auto& chatHandler : chatHandlers_) {
     500            2 :         if (reinterpret_cast<uintptr_t>(chatHandler.get()) == chatHandlerId) {
     501            2 :             return chatHandler->getChatHandlerDetails();
     502              :         }
     503              :     }
     504            0 :     return {};
     505            2 : }
     506              : 
     507              : std::set<std::pair<std::string, std::string>>
     508            0 : ChatServicesManager::getEnabledConversationsForPlugin(const DLPlugin* plugin) const
     509              : {
     510            0 :     if (!plugin) {
     511            0 :         return {};
     512              :     }
     513              : 
     514            0 :     std::lock_guard const lk(mtx_);
     515              :     // Strip the .so filename to get the plugin's rootPath.
     516            0 :     const auto& soPath = plugin->getPath();
     517            0 :     const auto sep = soPath.find_last_of(DIR_SEPARATOR_CH);
     518            0 :     const std::string rootPath = sep != std::string::npos ? soPath.substr(0, sep) : soPath;
     519              : 
     520              :     // Collect handler pointers and names that belong to this plugin.
     521            0 :     std::set<uintptr_t> handlerPtrs;
     522            0 :     std::set<std::string> handlerNames;
     523            0 :     for (const auto& handler : chatHandlers_) {
     524            0 :         const auto& hid = handler->id();
     525            0 :         const auto sep2 = hid.find_last_of(DIR_SEPARATOR_CH);
     526            0 :         const std::string handlerRoot = sep2 != std::string::npos ? hid.substr(0, sep2) : hid;
     527            0 :         if (handlerRoot == rootPath) {
     528            0 :             handlerPtrs.insert(reinterpret_cast<uintptr_t>(handler.get()));
     529            0 :             handlerNames.insert(handler->getChatHandlerDetails().at("name"));
     530              :         }
     531            0 :     }
     532              : 
     533            0 :     std::set<std::pair<std::string, std::string>> result;
     534              : 
     535            0 :     for (const auto& [key, toggledSet] : chatHandlerToggled_) {
     536            0 :         for (auto ptr : handlerPtrs) {
     537            0 :             if (toggledSet.contains(ptr)) {
     538            0 :                 result.insert(key);
     539            0 :                 break;
     540              :             }
     541              :         }
     542              :     }
     543              : 
     544            0 :     for (const auto& [key, nameMap] : allowDenyList_) {
     545            0 :         for (const auto& name : handlerNames) {
     546            0 :             if (const auto it = nameMap.find(name); it != nameMap.end() && it->second) {
     547            0 :                 result.insert(key);
     548            0 :                 break;
     549              :             }
     550              :         }
     551              :     }
     552              : 
     553            0 :     for (const auto& [a, c] : result) {
     554            0 :         JAMI_DEBUG("getEnabledConversationsForPlugin: plugin {} -> {}/{}", rootPath, a, c);
     555              :     }
     556              : 
     557            0 :     return result;
     558            0 : }
     559              : 
     560              : bool
     561            2 : ChatServicesManager::setPreference(const std::string& key, const std::string& value, const std::string& rootPath)
     562              : {
     563              :     // Collect matching handlers under mtx_, then call setPreferenceAttribute outside the lock.
     564              :     // setPreferenceAttribute callbacks into invokeService (clearBodyOverwritesAllConversations,
     565              :     // reloadBodyOverwritesAllConversations) which acquires mtx_ — holding it here deadlocks.
     566            2 :     std::vector<ChatHandlerPtr> toNotify;
     567              :     {
     568            2 :         std::lock_guard const lk(mtx_);
     569            2 :         for (auto& chatHandler : chatHandlers_) {
     570            0 :             if (chatHandler->id().find(rootPath) != std::string::npos && chatHandler->preferenceMapHasKey(key)) {
     571            0 :                 toNotify.push_back(chatHandler);
     572              :             }
     573              :         }
     574            2 :     } // release mtx_
     575              : 
     576            2 :     for (const auto& handler : toNotify) {
     577            0 :         handler->setPreferenceAttribute(key, value);
     578              :     }
     579              : 
     580            4 :     return toNotify.empty(); // false = key was handled → caller skips plugin reload
     581            2 : }
     582              : 
     583              : void
     584            2 : ChatServicesManager::toggleChatHandler(const uintptr_t chatHandlerId,
     585              :                                        const std::string& accountId,
     586              :                                        const std::string& peerId,
     587              :                                        const bool toggle)
     588              : {
     589            2 :     std::pair<std::string, std::string> mPair(accountId, peerId);
     590            2 :     chatSubjectPtr subject;
     591            2 :     ChatHandler* handler = nullptr;
     592              : 
     593              :     {
     594            2 :         std::lock_guard const lk(mtx_);
     595            2 :         auto& handlers = chatHandlerToggled_[mPair];
     596            2 :         auto& chatAllowDenySet = allowDenyList_[mPair];
     597            2 :         subject = chatSubjects_.emplace(mPair, std::make_shared<PublishObservable<pluginMessagePtr>>()).first->second;
     598              : 
     599            2 :         auto chatHandlerIt = std::find_if(chatHandlers_.begin(),
     600              :                                           chatHandlers_.end(),
     601            2 :                                           [chatHandlerId](const ChatHandlerPtr& h) {
     602            2 :                                               return reinterpret_cast<uintptr_t>(h.get()) == chatHandlerId;
     603              :                                           });
     604              : 
     605            2 :         if (chatHandlerIt != chatHandlers_.end()) {
     606            2 :             handler = chatHandlerIt->get();
     607            6 :             const auto handlerName = (*chatHandlerIt)->getChatHandlerDetails().at("name");
     608            2 :             if (toggle) {
     609            1 :                 handlers.insert(chatHandlerId);
     610            1 :                 chatAllowDenySet[handlerName] = true;
     611              :             } else {
     612            1 :                 handlers.erase(chatHandlerId);
     613            1 :                 chatAllowDenySet[handlerName] = false;
     614              :             }
     615            2 :         }
     616            2 :     } // release mtx_
     617              : 
     618            2 :     if (handler) {
     619            8 :         JAMI_DEBUG("toggleChatHandler: {} handler {} for {}/{}",
     620              :                    toggle ? "activating" : "deactivating",
     621              :                    chatHandlerId,
     622              :                    accountId,
     623              :                    peerId);
     624            2 :         if (toggle) {
     625            1 :             handler->notifyChatSubject(mPair, subject);
     626              :         } else {
     627            1 :             handler->detach(subject);
     628            1 :             if (const auto acc = Manager::instance().getAccount<JamiAccount>(accountId)) {
     629            1 :                 if (auto* convMod = acc->convModule()) {
     630            1 :                     if (const auto conv = convMod->getConversation(peerId)) {
     631            1 :                         conv->clearBodyOverwrites();
     632            1 :                         conv->loadMissingBodyOverwrites();
     633            1 :                     }
     634              :                 }
     635            1 :             }
     636              :         }
     637            2 :         std::lock_guard const lk(mtx_);
     638            2 :         PluginPreferencesUtils::setAllowDenyListPreferences(allowDenyList_);
     639            2 :     } else {
     640            0 :         JAMI_DEBUG("toggleChatHandler: handler {} not found", chatHandlerId);
     641              :     }
     642            2 : }
     643              : } // namespace jami
        

Generated by: LCOV version 2.0-1