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 "presence_manager.h"
19 : #include "jami_contact.h" // For DeviceAnnouncement
20 : #include "logger.h"
21 :
22 : namespace jami {
23 :
24 722 : PresenceManager::PresenceManager(const std::shared_ptr<dht::DhtRunner>& dht)
25 722 : : dht_(dht)
26 722 : {}
27 :
28 722 : PresenceManager::~PresenceManager()
29 : {
30 722 : std::lock_guard lock(mutex_);
31 1018 : for (auto& [h, buddy] : trackedBuddies_) {
32 296 : if (dht_ && dht_->isRunning()) {
33 0 : dht_->cancelListen(h, std::move(buddy.listenToken));
34 : }
35 : }
36 722 : }
37 :
38 : void
39 828 : PresenceManager::trackBuddy(const std::string& uri)
40 : {
41 828 : dht::InfoHash h(uri);
42 828 : if (!h)
43 0 : return;
44 :
45 828 : std::lock_guard lock(mutex_);
46 828 : auto it = trackedBuddies_.find(h);
47 828 : if (it == trackedBuddies_.end()) {
48 797 : it = trackedBuddies_.emplace(h, TrackedBuddy {h}).first;
49 : }
50 :
51 828 : it->second.refCount++;
52 828 : if (it->second.refCount == 1) {
53 797 : trackPresence(h, it->second);
54 : }
55 828 : }
56 :
57 : void
58 520 : PresenceManager::untrackBuddy(const std::string& uri)
59 : {
60 520 : dht::InfoHash h(uri);
61 520 : if (!h)
62 0 : return;
63 :
64 520 : std::lock_guard lock(mutex_);
65 520 : auto it = trackedBuddies_.find(h);
66 520 : if (it != trackedBuddies_.end()) {
67 520 : it->second.refCount--;
68 519 : if (it->second.refCount <= 0) {
69 501 : if (dht_ && dht_->isRunning()) {
70 497 : dht_->cancelListen(h, std::move(it->second.listenToken));
71 : }
72 501 : trackedBuddies_.erase(it);
73 : }
74 : }
75 520 : }
76 :
77 : bool
78 2286 : PresenceManager::isOnline(const std::string& uri) const
79 : {
80 2286 : dht::InfoHash h(uri);
81 2286 : if (!h)
82 0 : return false;
83 2286 : std::lock_guard lock(mutex_);
84 2286 : auto it = trackedBuddies_.find(h);
85 2286 : return it != trackedBuddies_.end() && !it->second.deviceValues.empty();
86 2286 : }
87 :
88 : std::map<std::string, bool>
89 2 : PresenceManager::getTrackedBuddyPresence() const
90 : {
91 2 : std::lock_guard lock(mutex_);
92 2 : std::map<std::string, bool> presence_info;
93 4 : for (const auto& [h, buddy] : trackedBuddies_) {
94 2 : presence_info.emplace(h.toString(), !buddy.deviceValues.empty());
95 : }
96 4 : return presence_info;
97 2 : }
98 :
99 : std::vector<dht::PkId>
100 1019 : PresenceManager::getDevices(const std::string& uri) const
101 : {
102 1019 : dht::InfoHash h(uri);
103 1019 : if (!h)
104 0 : return {};
105 1019 : std::lock_guard lock(mutex_);
106 1019 : auto it = trackedBuddies_.find(h);
107 1019 : if (it == trackedBuddies_.end())
108 214 : return {};
109 805 : std::vector<dht::PkId> devices;
110 805 : devices.reserve(it->second.deviceValues.size());
111 949 : for (const auto& [deviceId, valueIds] : it->second.deviceValues)
112 144 : devices.emplace_back(deviceId);
113 805 : return devices;
114 1019 : }
115 :
116 : uint64_t
117 737 : PresenceManager::addListener(PresenceCallback cb)
118 : {
119 737 : std::lock_guard lock(listenersMutex_);
120 737 : auto id = nextListenerId_++;
121 737 : listeners_.emplace(id, std::move(cb));
122 737 : return id;
123 737 : }
124 :
125 : void
126 0 : PresenceManager::removeListener(uint64_t token)
127 : {
128 0 : std::lock_guard lock(listenersMutex_);
129 0 : listeners_.erase(token);
130 0 : }
131 :
132 : uint64_t
133 1123 : PresenceManager::addDeviceListener(DevicePresenceCallback cb)
134 : {
135 1123 : std::lock_guard lock(listenersMutex_);
136 1123 : auto id = nextListenerId_++;
137 1123 : deviceListeners_.emplace(id, std::move(cb));
138 1123 : return id;
139 1123 : }
140 :
141 : void
142 219 : PresenceManager::removeDeviceListener(uint64_t token)
143 : {
144 219 : std::lock_guard lock(listenersMutex_);
145 219 : deviceListeners_.erase(token);
146 218 : }
147 :
148 : void
149 636 : PresenceManager::refresh()
150 : {
151 636 : std::lock_guard lock(mutex_);
152 649 : for (auto& [h, buddy] : trackedBuddies_) {
153 13 : buddy.listenToken = {};
154 13 : buddy.deviceValues.clear();
155 13 : trackPresence(h, buddy);
156 : }
157 636 : }
158 :
159 : void
160 810 : PresenceManager::trackPresence(const dht::InfoHash& h, TrackedBuddy& buddy)
161 : {
162 810 : if (!dht_ || !dht_->isRunning())
163 10 : return;
164 :
165 800 : if (buddy.listenToken.valid()) {
166 0 : JAMI_ERROR("PresenceManager: Already tracking presence for {}", h.toString());
167 0 : return;
168 : }
169 :
170 3200 : buddy.listenToken = dht_->listen(
171 : h,
172 800 : [this, h](const std::vector<std::shared_ptr<dht::Value>>& values, bool expired) {
173 : // A contact device can be advertised by more than one DeviceAnnouncement
174 : // value at the same time (e.g. a freshly re-generated announcement coexisting
175 : // with a previous one that is still living out its DHT TTL). All of them
176 : // decode to the same device id. We therefore reference-count, per device, the
177 : // set of live announcement value ids: a device stays present as long as at
178 : // least one of its announcement values is alive, and only goes offline once
179 : // the last one expires. Toggling the device on every single value expiry would
180 : // mark the contact offline while a healthy announcement is still on the DHT.
181 :
182 : // Decode and validate the announcements before taking the lock, so the
183 : // (potentially expensive) msgpack/crypto decoding does not block unrelated
184 : // presence queries or listener updates.
185 704 : std::vector<std::pair<dht::PkId, dht::Value::Id>> decoded;
186 704 : decoded.reserve(values.size());
187 1429 : for (const auto& value : values) {
188 : try {
189 725 : auto dev = dht::Value::unpack<DeviceAnnouncement>(*value);
190 725 : if (!dev.pk) {
191 0 : JAMI_WARNING("PresenceManager: Received DeviceAnnouncement without public "
192 : "key for {}",
193 : h.toString());
194 0 : continue;
195 : }
196 725 : decoded.emplace_back(dev.pk->getLongId(), value->id);
197 725 : } catch (const std::exception& e) {
198 0 : JAMI_WARNING("PresenceManager: Failed to decode DeviceAnnouncement {} for {}: {}",
199 : value->id,
200 : h.toString(),
201 : e.what());
202 0 : }
203 : }
204 :
205 704 : std::vector<std::pair<dht::PkId, bool>> deviceChanges;
206 : bool wasConnected, isConnected;
207 : {
208 704 : std::lock_guard lock(mutex_);
209 704 : auto it = trackedBuddies_.find(h);
210 704 : if (it == trackedBuddies_.end())
211 12 : return true;
212 :
213 692 : auto& deviceValues = it->second.deviceValues;
214 692 : wasConnected = !deviceValues.empty();
215 :
216 1402 : for (const auto& [deviceId, valueId] : decoded) {
217 710 : if (expired) {
218 72 : auto dit = deviceValues.find(deviceId);
219 72 : if (dit != deviceValues.end()) {
220 72 : dit->second.erase(valueId);
221 72 : if (dit->second.empty()) {
222 70 : deviceValues.erase(dit);
223 70 : deviceChanges.emplace_back(deviceId, false);
224 : }
225 : }
226 : } else {
227 638 : auto& ids = deviceValues[deviceId];
228 638 : bool deviceWasAbsent = ids.empty();
229 638 : ids.insert(valueId);
230 638 : if (deviceWasAbsent)
231 634 : deviceChanges.emplace_back(deviceId, true);
232 : }
233 : }
234 :
235 692 : isConnected = !deviceValues.empty();
236 704 : }
237 :
238 1396 : for (const auto& [deviceId, deviceOnline] : deviceChanges)
239 704 : notifyDeviceListeners(h.toString(), deviceId, deviceOnline);
240 :
241 692 : if (isConnected != wasConnected) {
242 626 : notifyListeners(h.toString(), isConnected);
243 : }
244 692 : return true;
245 704 : },
246 2400 : dht::getFilterSet<DeviceAnnouncement>());
247 : }
248 :
249 : void
250 626 : PresenceManager::notifyListeners(const std::string& uri, bool online)
251 : {
252 626 : std::vector<PresenceCallback> cbs;
253 : {
254 626 : std::lock_guard lock(listenersMutex_);
255 626 : cbs.reserve(listeners_.size());
256 1257 : for (const auto& [id, cb] : listeners_) {
257 631 : cbs.emplace_back(cb);
258 : }
259 626 : }
260 1257 : for (const auto& cb : cbs) {
261 631 : cb(uri, online);
262 : }
263 626 : }
264 :
265 : void
266 704 : PresenceManager::notifyDeviceListeners(const std::string& uri, const dht::PkId& deviceId, bool online)
267 : {
268 704 : std::vector<DevicePresenceCallback> cbs;
269 : {
270 704 : std::lock_guard lock(listenersMutex_);
271 704 : cbs.reserve(deviceListeners_.size());
272 2096 : for (const auto& [id, cb] : deviceListeners_) {
273 1392 : cbs.emplace_back(cb);
274 : }
275 704 : }
276 2096 : for (const auto& cb : cbs) {
277 1392 : cb(uri, deviceId, online);
278 : }
279 704 : }
280 :
281 : } // namespace jami
|