librats 0.7.2 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +9 -4
  2. package/lib/index.d.ts +9 -22
  3. package/native-src/CMakeLists.txt +132 -50
  4. package/native-src/cmake/ratsConfig.cmake.in +8 -0
  5. package/native-src/src/bt_network.cpp +296 -183
  6. package/native-src/src/bt_network.h +25 -5
  7. package/native-src/src/crypto/sha256.c +1 -1
  8. package/native-src/src/dht.cpp +297 -47
  9. package/native-src/src/dht.h +70 -6
  10. package/native-src/src/file_transfer.cpp +1185 -1578
  11. package/native-src/src/file_transfer.h +240 -521
  12. package/native-src/src/io_poller.cpp +917 -0
  13. package/native-src/src/io_poller.h +138 -0
  14. package/native-src/src/krpc.cpp +161 -96
  15. package/native-src/src/krpc.h +18 -4
  16. package/native-src/src/librats.cpp +812 -1236
  17. package/native-src/src/librats.h +207 -208
  18. package/native-src/src/librats_bittorrent.cpp +1 -5
  19. package/native-src/src/librats_c.cpp +22 -39
  20. package/native-src/src/librats_discovery.cpp +377 -0
  21. package/native-src/src/librats_encryption.cpp +130 -283
  22. package/native-src/src/librats_file_transfer.cpp +27 -109
  23. package/native-src/src/librats_gossipsub.cpp +1 -5
  24. package/native-src/src/librats_ice.cpp +5 -1
  25. package/native-src/src/librats_log_macros.h +36 -0
  26. package/native-src/src/librats_logging.cpp +1 -7
  27. package/native-src/src/librats_mdns.cpp +1 -11
  28. package/native-src/src/librats_persistence.cpp +1 -11
  29. package/native-src/src/librats_reconnection.cpp +2 -13
  30. package/native-src/src/librats_statistic.cpp +105 -0
  31. package/native-src/src/socket.cpp +15 -3
  32. package/package.json +1 -1
  33. package/scripts/build-librats.js +3 -0
  34. package/scripts/prepare-package.js +10 -0
  35. package/src/librats_node.cpp +53 -64
@@ -679,42 +679,36 @@ char* rats_send_directory(rats_client_t handle, const char* peer_id, const char*
679
679
  rats_client_wrapper* wrap = static_cast<rats_client_wrapper*>(handle);
680
680
 
681
681
  std::string remote_name = remote_directory_name ? std::string(remote_directory_name) : std::string("");
682
- std::string transfer_id = wrap->client->send_directory(std::string(peer_id), std::string(directory_path), remote_name, recursive != 0);
683
-
682
+ (void)recursive; // directory transfers are always recursive
683
+ std::string transfer_id = wrap->client->send_directory(std::string(peer_id), std::string(directory_path), remote_name);
684
+
684
685
  return transfer_id.empty() ? nullptr : rats_strdup_owned(transfer_id);
685
686
  }
686
687
 
688
+ // Pull-style requests are not supported in this version; transfers are push-only.
687
689
  char* rats_request_file(rats_client_t handle, const char* peer_id, const char* remote_file_path, const char* local_path) {
688
- if (!handle || !peer_id || !remote_file_path || !local_path) return nullptr;
689
- rats_client_wrapper* wrap = static_cast<rats_client_wrapper*>(handle);
690
-
691
- std::string transfer_id = wrap->client->request_file(std::string(peer_id), std::string(remote_file_path), std::string(local_path));
692
-
693
- return transfer_id.empty() ? nullptr : rats_strdup_owned(transfer_id);
690
+ (void)handle; (void)peer_id; (void)remote_file_path; (void)local_path;
691
+ return nullptr;
694
692
  }
695
693
 
696
694
  char* rats_request_directory(rats_client_t handle, const char* peer_id, const char* remote_directory_path, const char* local_directory_path, int recursive) {
697
- if (!handle || !peer_id || !remote_directory_path || !local_directory_path) return nullptr;
698
- rats_client_wrapper* wrap = static_cast<rats_client_wrapper*>(handle);
699
-
700
- std::string transfer_id = wrap->client->request_directory(std::string(peer_id), std::string(remote_directory_path), std::string(local_directory_path), recursive != 0);
701
-
702
- return transfer_id.empty() ? nullptr : rats_strdup_owned(transfer_id);
695
+ (void)handle; (void)peer_id; (void)remote_directory_path; (void)local_directory_path; (void)recursive;
696
+ return nullptr;
703
697
  }
704
698
 
705
699
  rats_error_t rats_accept_directory_transfer(rats_client_t handle, const char* transfer_id, const char* local_path) {
706
700
  if (!handle || !transfer_id || !local_path) return RATS_ERROR_INVALID_PARAMETER;
707
701
  rats_client_wrapper* wrap = static_cast<rats_client_wrapper*>(handle);
708
-
709
- return wrap->client->accept_directory_transfer(std::string(transfer_id), std::string(local_path)) ? RATS_SUCCESS : RATS_ERROR_OPERATION_FAILED;
702
+
703
+ return wrap->client->accept_file_transfer(std::string(transfer_id), std::string(local_path)) ? RATS_SUCCESS : RATS_ERROR_OPERATION_FAILED;
710
704
  }
711
705
 
712
706
  rats_error_t rats_reject_directory_transfer(rats_client_t handle, const char* transfer_id, const char* reason) {
713
707
  if (!handle || !transfer_id) return RATS_ERROR_INVALID_PARAMETER;
714
708
  rats_client_wrapper* wrap = static_cast<rats_client_wrapper*>(handle);
715
-
709
+
716
710
  std::string reject_reason = reason ? std::string(reason) : std::string("");
717
- return wrap->client->reject_directory_transfer(std::string(transfer_id), reject_reason) ? RATS_SUCCESS : RATS_ERROR_OPERATION_FAILED;
711
+ return wrap->client->reject_file_transfer(std::string(transfer_id), reject_reason) ? RATS_SUCCESS : RATS_ERROR_OPERATION_FAILED;
718
712
  }
719
713
 
720
714
  rats_error_t rats_pause_file_transfer(rats_client_t handle, const char* transfer_id) {
@@ -768,44 +762,33 @@ void rats_set_file_request_callback(rats_client_t handle, rats_file_request_cb c
768
762
  wrap->file_request_ud = user_data;
769
763
 
770
764
  if (cb) {
771
- wrap->client->on_file_request([wrap](const std::string& peer_id, const std::string& file_path, const std::string& transfer_id) {
765
+ // Fires for every incoming offer (file or directory). The C app should
766
+ // then call rats_accept_file_transfer()/rats_reject_file_transfer().
767
+ wrap->client->on_file_transfer_request([wrap](const IncomingTransferOffer& offer) {
772
768
  if (wrap->file_request_cb) {
773
- wrap->file_request_cb(wrap->file_request_ud, peer_id.c_str(), transfer_id.c_str(), file_path.c_str(), "");
769
+ wrap->file_request_cb(wrap->file_request_ud, offer.peer_id.c_str(),
770
+ offer.transfer_id.c_str(), "", offer.name.c_str());
774
771
  }
775
- return true; // Accept the file request
776
772
  });
777
773
  }
778
774
  }
779
775
 
776
+ // Directory offers arrive through the unified file-request callback above;
777
+ // this setter is kept for ABI compatibility and simply records the callback.
780
778
  void rats_set_directory_request_callback(rats_client_t handle, rats_directory_request_cb cb, void* user_data) {
781
779
  if (!handle) return;
782
780
  rats_client_wrapper* wrap = static_cast<rats_client_wrapper*>(handle);
783
781
  wrap->directory_request_cb = cb;
784
782
  wrap->directory_request_ud = user_data;
785
-
786
- if (cb) {
787
- wrap->client->on_directory_request([wrap](const std::string& peer_id, const std::string& directory_path, bool recursive, const std::string& transfer_id) {
788
- if (wrap->directory_request_cb) {
789
- wrap->directory_request_cb(wrap->directory_request_ud, peer_id.c_str(), transfer_id.c_str(), directory_path.c_str(), "");
790
- }
791
- return true; // Accept the directory request
792
- });
793
- }
794
783
  }
795
784
 
785
+ // Directory progress arrives through the unified file-progress callback above;
786
+ // this setter is kept for ABI compatibility and simply records the callback.
796
787
  void rats_set_directory_progress_callback(rats_client_t handle, rats_directory_progress_cb cb, void* user_data) {
797
788
  if (!handle) return;
798
789
  rats_client_wrapper* wrap = static_cast<rats_client_wrapper*>(handle);
799
790
  wrap->directory_progress_cb = cb;
800
791
  wrap->directory_progress_ud = user_data;
801
-
802
- if (cb) {
803
- wrap->client->on_directory_transfer_progress([wrap](const std::string& transfer_id, const std::string& current_file, uint64_t files_completed, uint64_t total_files, uint64_t bytes_completed, uint64_t total_bytes) {
804
- if (wrap->directory_progress_cb) {
805
- wrap->directory_progress_cb(wrap->directory_progress_ud, transfer_id.c_str(), static_cast<int>(files_completed), static_cast<int>(total_files), current_file.c_str());
806
- }
807
- });
808
- }
809
792
  }
810
793
 
811
794
  // Peer information
@@ -847,7 +830,7 @@ char* rats_get_peer_info_json(rats_client_t handle, const char* peer_id) {
847
830
  if (!handle || !peer_id) return nullptr;
848
831
  rats_client_wrapper* wrap = static_cast<rats_client_wrapper*>(handle);
849
832
 
850
- const RatsPeer* peer = wrap->client->get_peer_by_id(std::string(peer_id));
833
+ auto peer = wrap->client->get_peer_by_id(std::string(peer_id));
851
834
  if (!peer) return nullptr;
852
835
 
853
836
  // Create JSON representation of peer info
@@ -0,0 +1,377 @@
1
+ #include "librats.h"
2
+ #include "librats_log_macros.h"
3
+ #include "sha1.h"
4
+
5
+ namespace librats {
6
+
7
+ // =========================================================================
8
+ // Peer Discovery Methods
9
+ // =========================================================================
10
+
11
+ bool RatsClient::start_dht_discovery(int dht_port) {
12
+ if (dht_client_ && dht_client_->is_running()) {
13
+ LOG_CLIENT_WARN("DHT discovery is already running");
14
+ return true;
15
+ }
16
+
17
+ LOG_CLIENT_INFO("Starting DHT discovery on port " << dht_port <<
18
+ (bind_address_.empty() ? "" : " bound to " + bind_address_));
19
+
20
+ auto bootstrap_nodes = DhtClient::get_default_bootstrap_nodes();
21
+
22
+ // IPv4 DHT (also shared with the BitTorrent subsystem) - required.
23
+ dht_client_ = std::make_unique<DhtClient>(dht_port, bind_address_, data_directory_, AddressFamily::IPv4);
24
+ if (!dht_client_->start()) {
25
+ LOG_CLIENT_ERROR("Failed to start IPv4 DHT client");
26
+ dht_client_.reset();
27
+ return false;
28
+ }
29
+ if (!dht_client_->bootstrap(bootstrap_nodes)) {
30
+ LOG_CLIENT_WARN("Failed to bootstrap IPv4 DHT");
31
+ }
32
+
33
+ // IPv6 DHT - best effort. Shares the same port via a V6ONLY socket. If the host has
34
+ // no usable IPv6 stack, socket creation fails and we simply run IPv4-only.
35
+ dht_client_v6_ = std::make_unique<DhtClient>(dht_port, bind_address_, data_directory_, AddressFamily::IPv6);
36
+ if (dht_client_v6_->start()) {
37
+ if (!dht_client_v6_->bootstrap(bootstrap_nodes)) {
38
+ LOG_CLIENT_WARN("Failed to bootstrap IPv6 DHT");
39
+ }
40
+ LOG_CLIENT_INFO("IPv6 DHT client started");
41
+ } else {
42
+ LOG_CLIENT_INFO("IPv6 DHT unavailable on this host, running IPv4-only");
43
+ dht_client_v6_.reset();
44
+ }
45
+
46
+ // Start automatic peer discovery (this thread also performs the best-effort STUN probe
47
+ // that derives our BEP 42 node ID — see automatic_discovery_loop).
48
+ start_automatic_peer_discovery();
49
+
50
+ LOG_CLIENT_INFO("DHT discovery started successfully");
51
+ return true;
52
+ }
53
+
54
+ void RatsClient::stop_dht_discovery() {
55
+ if (!dht_client_ && !dht_client_v6_) {
56
+ return;
57
+ }
58
+
59
+ LOG_CLIENT_INFO("Stopping DHT discovery");
60
+
61
+ // Stop automatic peer discovery
62
+ stop_automatic_peer_discovery();
63
+
64
+ if (dht_client_) {
65
+ dht_client_->stop();
66
+ dht_client_.reset();
67
+ }
68
+ if (dht_client_v6_) {
69
+ dht_client_v6_->stop();
70
+ dht_client_v6_.reset();
71
+ }
72
+ LOG_CLIENT_INFO("DHT discovery stopped");
73
+ }
74
+
75
+ bool RatsClient::find_peers_by_hash(const std::string& content_hash, std::function<void(const std::vector<std::string>&)> callback) {
76
+ if (!is_dht_running()) {
77
+ LOG_CLIENT_ERROR("DHT client not running");
78
+ return false;
79
+ }
80
+
81
+ if (content_hash.length() != CONTENT_HASH_HEX_LENGTH) {
82
+ LOG_CLIENT_ERROR("Invalid content hash length: " << content_hash.length() << " (expected " << CONTENT_HASH_HEX_LENGTH << ")");
83
+ return false;
84
+ }
85
+
86
+ LOG_CLIENT_INFO("Finding peers for content hash: " << content_hash);
87
+
88
+ InfoHash info_hash = hex_to_node_id(content_hash);
89
+
90
+ // Shared wrapper: discovered peers (IPv4 or IPv6) are delivered through the same callback.
91
+ PeerDiscoveryCallback peer_callback = [callback](const std::vector<Peer>& peers, const InfoHash&) {
92
+ std::vector<std::string> peer_addresses;
93
+ peer_addresses.reserve(peers.size());
94
+ for (const auto& peer : peers) {
95
+ peer_addresses.emplace_back(peer.ip + ":" + std::to_string(peer.port));
96
+ }
97
+ if (callback) {
98
+ callback(peer_addresses);
99
+ }
100
+ };
101
+
102
+ // Fan out to both Kademlia networks; succeed if at least one accepts the search.
103
+ bool started = false;
104
+ if (dht_client_ && dht_client_->is_running()) {
105
+ started |= dht_client_->find_peers(info_hash, peer_callback);
106
+ }
107
+ if (dht_client_v6_ && dht_client_v6_->is_running()) {
108
+ started |= dht_client_v6_->find_peers(info_hash, peer_callback);
109
+ }
110
+ return started;
111
+ }
112
+
113
+ bool RatsClient::announce_for_hash(const std::string& content_hash, uint16_t port,
114
+ std::function<void(const std::vector<std::string>&)> callback) {
115
+ if (!is_dht_running()) {
116
+ LOG_CLIENT_ERROR("DHT client not running");
117
+ return false;
118
+ }
119
+
120
+ if (content_hash.length() != CONTENT_HASH_HEX_LENGTH) {
121
+ LOG_CLIENT_ERROR("Invalid content hash length: " << content_hash.length() << " (expected " << CONTENT_HASH_HEX_LENGTH << ")");
122
+ return false;
123
+ }
124
+
125
+ if (port == 0) {
126
+ port = listen_port_;
127
+ }
128
+
129
+ LOG_CLIENT_INFO("Announcing for content hash: " << content_hash << " on port " << port
130
+ << (callback ? " with peer callback" : ""));
131
+
132
+ InfoHash info_hash = hex_to_node_id(content_hash);
133
+
134
+ // Create wrapper callback that converts Peer to string addresses (if callback provided)
135
+ PeerDiscoveryCallback peer_callback = nullptr;
136
+ if (callback) {
137
+ peer_callback = [callback](const std::vector<Peer>& peers, const InfoHash& hash) {
138
+ std::vector<std::string> peer_addresses;
139
+ peer_addresses.reserve(peers.size());
140
+ for (const auto& peer : peers) {
141
+ peer_addresses.push_back(peer.ip + ":" + std::to_string(peer.port));
142
+ }
143
+ callback(peer_addresses);
144
+ };
145
+ }
146
+
147
+ // Announce on both Kademlia networks so IPv4 and IPv6 peers can both find us.
148
+ bool started = false;
149
+ if (dht_client_ && dht_client_->is_running()) {
150
+ started |= dht_client_->announce_peer(info_hash, port, peer_callback);
151
+ }
152
+ if (dht_client_v6_ && dht_client_v6_->is_running()) {
153
+ started |= dht_client_v6_->announce_peer(info_hash, port, peer_callback);
154
+ }
155
+ return started;
156
+ }
157
+
158
+ bool RatsClient::is_dht_running() const {
159
+ return (dht_client_ && dht_client_->is_running()) ||
160
+ (dht_client_v6_ && dht_client_v6_->is_running());
161
+ }
162
+
163
+ size_t RatsClient::get_dht_routing_table_size() const {
164
+ size_t total = 0;
165
+ if (dht_client_) {
166
+ total += dht_client_->get_routing_table_size();
167
+ }
168
+ if (dht_client_v6_) {
169
+ total += dht_client_v6_->get_routing_table_size();
170
+ }
171
+ return total;
172
+ }
173
+
174
+ void RatsClient::handle_dht_peer_discovery(const std::vector<Peer>& peers, const InfoHash& info_hash) {
175
+ LOG_CLIENT_INFO("DHT discovered " << peers.size() << " peers for info hash: " << node_id_to_hex(info_hash));
176
+
177
+ // Auto-connect to discovered peers
178
+ for (const auto& peer : peers) {
179
+ if (!can_connect_to_peer(peer.ip, peer.port)) {
180
+ continue;
181
+ }
182
+
183
+ LOG_CLIENT_DEBUG("Attempting to connect to discovered peer: " << peer.ip << ":" << peer.port);
184
+
185
+ // Try to connect to the peer (non-blocking, managed thread for graceful shutdown)
186
+ add_managed_thread(std::thread([this, peer]() {
187
+ if (connect_to_peer(peer.ip, peer.port)) {
188
+ LOG_CLIENT_INFO("Successfully connected to DHT discovered peer: " << peer.ip << ":" << peer.port);
189
+ } else {
190
+ LOG_CLIENT_DEBUG("Failed to connect to DHT discovered peer: " << peer.ip << ":" << peer.port);
191
+ }
192
+ }), "dht-connect-" + peer.ip);
193
+ }
194
+ }
195
+
196
+ void RatsClient::start_automatic_peer_discovery() {
197
+ if (auto_discovery_running_.load()) {
198
+ LOG_CLIENT_WARN("Automatic peer discovery is already running");
199
+ return;
200
+ }
201
+
202
+ LOG_CLIENT_INFO("Starting automatic rats peer discovery");
203
+ auto_discovery_running_.store(true);
204
+ auto_discovery_thread_ = std::thread(&RatsClient::automatic_discovery_loop, this);
205
+ }
206
+
207
+ void RatsClient::stop_automatic_peer_discovery() {
208
+ if (!auto_discovery_running_.load()) {
209
+ return;
210
+ }
211
+
212
+ LOG_CLIENT_INFO("Stopping automatic peer discovery");
213
+ auto_discovery_running_.store(false);
214
+
215
+ if (auto_discovery_thread_.joinable()) {
216
+ auto_discovery_thread_.join();
217
+ }
218
+
219
+ LOG_CLIENT_INFO("Automatic peer discovery stopped");
220
+ }
221
+
222
+ bool RatsClient::is_automatic_discovery_running() const {
223
+ return auto_discovery_running_.load();
224
+ }
225
+
226
+ std::chrono::seconds RatsClient::calculate_discovery_interval() const {
227
+ int peer_count = get_peer_count();
228
+
229
+ // No peers - aggressive discovery
230
+ if (peer_count == 0) {
231
+ return std::chrono::seconds(15);
232
+ }
233
+
234
+ // Calculate fill ratio
235
+ float fill_ratio = static_cast<float>(peer_count) / static_cast<float>(max_peers_);
236
+
237
+ // Graduated intervals based on fill ratio
238
+ if (fill_ratio < 0.25f) {
239
+ // Less than 25% full - still fairly aggressive
240
+ return std::chrono::seconds(60); // 1 minute
241
+ } else if (fill_ratio < 0.50f) {
242
+ // 25-50% full - moderate
243
+ return std::chrono::seconds(180); // 3 minutes
244
+ } else if (fill_ratio < 0.75f) {
245
+ // 50-75% full - relaxed
246
+ return std::chrono::seconds(600); // 10 minutes
247
+ } else {
248
+ // 75-100% full - very relaxed (mostly just re-announcing)
249
+ return std::chrono::seconds(1800); // 30 minutes
250
+ }
251
+ }
252
+
253
+ void RatsClient::automatic_discovery_loop() {
254
+ LOG_CLIENT_INFO("Automatic peer discovery loop started");
255
+
256
+ // Initial delay to let DHT bootstrap
257
+ {
258
+ std::unique_lock<std::mutex> lock(shutdown_mutex_);
259
+ if (shutdown_cv_.wait_for(lock, std::chrono::seconds(INITIAL_DISCOVERY_DELAY_SECONDS), [this] { return !auto_discovery_running_.load() || !running_.load(); })) {
260
+ LOG_CLIENT_INFO("Automatic peer discovery loop stopped during initial delay");
261
+ return;
262
+ }
263
+ }
264
+
265
+ // Best-effort: discover our public IP via STUN and derive a BEP 42 node ID from it before
266
+ // we announce, so we announce under the correct ID. This runs on the discovery thread (not
267
+ // a standalone one) on purpose: stop_dht_discovery() joins this thread via
268
+ // stop_automatic_peer_discovery() BEFORE resetting the DHT clients, so set_external_ip()
269
+ // can never touch a destroyed DhtClient. It complements the in-DHT "ip"-field voting, which
270
+ // keeps the node ID correct if the address changes or STUN is unavailable.
271
+ if (auto_discovery_running_.load() && running_.load()) {
272
+ auto mapped = discover_public_address("stun.l.google.com", 19302, 4000);
273
+ if (mapped) {
274
+ // set_external_ip validates the address family, so feeding both instances is safe:
275
+ // only the matching-family DHT adopts it. (A default IPv4 STUN server yields an IPv4
276
+ // address; the IPv6 node ID is learned organically from IPv6 DHT responses.)
277
+ if (dht_client_) {
278
+ dht_client_->set_external_ip(mapped->address);
279
+ }
280
+ if (dht_client_v6_) {
281
+ dht_client_v6_->set_external_ip(mapped->address);
282
+ }
283
+ }
284
+ }
285
+
286
+ // Announce immediately - this also discovers peers during traversal
287
+ announce_rats_peer();
288
+
289
+ auto last_announce = std::chrono::steady_clock::now();
290
+
291
+ while (auto_discovery_running_.load()) {
292
+ auto now = std::chrono::steady_clock::now();
293
+
294
+ // Announce combines both announcing our presence and discovering peers
295
+ // Interval scales based on peer count: aggressive when empty, relaxed when nearly full
296
+ auto interval = calculate_discovery_interval();
297
+
298
+ if (now - last_announce >= interval) {
299
+ LOG_CLIENT_DEBUG("Discovery interval: " << interval.count() << "s (peers: "
300
+ << get_peer_count() << "/" << max_peers_ << ")");
301
+ announce_rats_peer();
302
+ last_announce = now;
303
+ }
304
+
305
+ // Use conditional variable for responsive shutdown
306
+ {
307
+ std::unique_lock<std::mutex> lock(shutdown_mutex_);
308
+ if (shutdown_cv_.wait_for(lock, std::chrono::milliseconds(500), [this] { return !auto_discovery_running_.load() || !running_.load(); })) {
309
+ break;
310
+ }
311
+ }
312
+ }
313
+
314
+ LOG_CLIENT_INFO("Automatic peer discovery loop stopped");
315
+ }
316
+
317
+ void RatsClient::announce_rats_peer() {
318
+ if (!dht_client_ || !dht_client_->is_running()) {
319
+ LOG_CLIENT_WARN("DHT client not running, cannot announce peer");
320
+ return;
321
+ }
322
+
323
+ std::string discovery_hash = get_discovery_hash();
324
+ LOG_CLIENT_INFO("Announcing peer for discovery hash: " << discovery_hash << " on port " << listen_port_);
325
+
326
+ InfoHash info_hash = hex_to_node_id(discovery_hash);
327
+
328
+ // Skip only if every running DHT network already has this announce in flight.
329
+ // (announce_peer() also dedups internally, so this is just to avoid redundant work.)
330
+ bool v4_busy = !dht_client_ || !dht_client_->is_running() || dht_client_->is_announce_active(info_hash);
331
+ bool v6_busy = !dht_client_v6_ || !dht_client_v6_->is_running() || dht_client_v6_->is_announce_active(info_hash);
332
+ if (v4_busy && v6_busy) {
333
+ LOG_CLIENT_WARN("Announce already in progress for info hash: " << node_id_to_hex(info_hash));
334
+ return;
335
+ }
336
+
337
+ // Use announce with callback - combines announce and find_peers in one traversal
338
+ // Peers discovered during traversal will be returned through the callback
339
+ if (announce_for_hash(discovery_hash, listen_port_, [this, info_hash](const std::vector<std::string>& peer_addresses) {
340
+ LOG_CLIENT_INFO("Announce discovered " << peer_addresses.size() << " peers during traversal");
341
+
342
+ // Convert peer addresses to Peer objects for handle_dht_peer_discovery()
343
+ std::vector<Peer> peers;
344
+ peers.reserve(peer_addresses.size());
345
+ for (const auto& peer_address : peer_addresses) {
346
+ std::string ip;
347
+ int port;
348
+ if (parse_address_string(peer_address, ip, port)) {
349
+ peers.push_back(Peer(ip, port));
350
+ }
351
+ }
352
+
353
+ // Auto-connect to discovered peers
354
+ if (!peers.empty()) {
355
+ handle_dht_peer_discovery(peers, info_hash);
356
+ }
357
+ })) {
358
+ LOG_CLIENT_DEBUG("Successfully started announce with peer discovery for discovery hash");
359
+ } else {
360
+ LOG_CLIENT_WARN("Failed to announce peer for discovery");
361
+ }
362
+ }
363
+
364
+ std::string RatsClient::get_discovery_hash() const {
365
+ std::lock_guard<std::mutex> lock(protocol_config_mutex_);
366
+ // Generate discovery hash based on current protocol configuration
367
+ std::string discovery_string = custom_protocol_name_ + "_peer_discovery_v" + custom_protocol_version_;
368
+ return SHA1::hash(discovery_string);
369
+ }
370
+
371
+ std::string RatsClient::get_rats_peer_discovery_hash() {
372
+ // Well-known hash for rats peer discovery
373
+ // Compute SHA1 hash of "rats_peer_discovery_v1.0"
374
+ return SHA1::hash("rats_peer_discovery_v1.0");
375
+ }
376
+
377
+ }