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
|