From 760d5d9adab13edc090f64437415b41b229481f8 Mon Sep 17 00:00:00 2001 From: Michael Jurkoic Date: Tue, 12 Mar 2024 09:34:35 -0500 Subject: Pass references into lambda Unit tests for event publishing currently fail due to event validation. A signer will need to be implemented before tests pass. --- src/nostr_service.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'src/nostr_service.cpp') diff --git a/src/nostr_service.cpp b/src/nostr_service.cpp index 09be6e3..4f4aadc 100644 --- a/src/nostr_service.cpp +++ b/src/nostr_service.cpp @@ -112,12 +112,12 @@ tuple NostrService::publishEvent(Event event) PLOG_INFO << "Attempting to publish event to Nostr relays."; vector>> publishFutures; - for (string relay : this->_activeRelays) + for (const string& relay : this->_activeRelays) { - future> publishFuture = async([this, relay, event]() { + future> publishFuture = async([this, &relay, &event]() { return this->_client->send(event.serialize(), relay); }); - + publishFutures.push_back(move(publishFuture)); } -- cgit From 423536e49259d338499dd8f8afaf106be7360764 Mon Sep 17 00:00:00 2001 From: Michael Jurkoic Date: Sun, 17 Mar 2024 14:36:41 -0500 Subject: Open relay subscriptions for a filter request --- include/nostr.hpp | 18 +++++++++++++++++- src/nostr_service.cpp | 52 +++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 67 insertions(+), 3 deletions(-) (limited to 'src/nostr_service.cpp') diff --git a/include/nostr.hpp b/include/nostr.hpp index 8041efe..22d9956 100644 --- a/include/nostr.hpp +++ b/include/nostr.hpp @@ -142,12 +142,22 @@ public: */ std::tuple publishEvent(Event event); - // TODO: Add methods for reading events from relays. + /** + * @brief Queries all open relay connections for events matching the given set of filters. + * @returns A tuple of `RelayList` objects, of the form ``, indicating + * to which relays the request was successfully sent, and which relays did not successfully + * receive the request. + */ + std::tuple queryRelays(Filters filters); + + // TODO: Write a method that receives events for an active subscription. + // TODO: Write a method that closes active subscriptions. private: std::mutex _propertyMutex; RelayList _defaultRelays; RelayList _activeRelays; + std::unordered_map> _subscriptionIds; client::IWebSocketClient* _client; /** @@ -182,5 +192,11 @@ private: * @brief Closes the connection from the client to the given relay. */ void disconnect(std::string relay); + + /** + * @brief Generates a unique subscription ID that may be used to identify event requests. + * @returns A stringified UUID. + */ + std::string generateSubscriptionId(); }; } // namespace nostr diff --git a/src/nostr_service.cpp b/src/nostr_service.cpp index 4f4aadc..3025b96 100644 --- a/src/nostr_service.cpp +++ b/src/nostr_service.cpp @@ -1,3 +1,6 @@ +#include +#include +#include #include #include #include @@ -7,6 +10,9 @@ #include "client/web_socket_client.hpp" using std::async; +using boost::uuids::random_generator; +using boost::uuids::to_string; +using boost::uuids::uuid; using std::future; using std::lock_guard; using std::make_tuple; @@ -104,8 +110,6 @@ void NostrService::closeRelayConnections(RelayList relays) tuple NostrService::publishEvent(Event event) { - // TODO: Add validation function. - RelayList successfulRelays; RelayList failedRelays; @@ -141,6 +145,44 @@ tuple NostrService::publishEvent(Event event) return make_tuple(successfulRelays, failedRelays); }; +tuple NostrService::queryRelays(Filters filters) +{ + RelayList successfulRelays; + RelayList failedRelays; + + vector>> requestFutures; + for (const string relay : this->_activeRelays) + { + string subscriptionId = this->generateSubscriptionId(); + this->_subscriptionIds[relay].push_back(subscriptionId); + string request = filters.serialize(subscriptionId); + + future> requestFuture = async([this, &relay, &request]() { + return this->_client->send(request, relay); + }); + requestFutures.push_back(move(requestFuture)); + } + + for (auto& publishFuture : requestFutures) + { + auto [relay, isSuccess] = publishFuture.get(); + if (isSuccess) + { + successfulRelays.push_back(relay); + } + else + { + failedRelays.push_back(relay); + } + } + + size_t targetCount = this->_activeRelays.size(); + size_t successfulCount = successfulRelays.size(); + PLOG_INFO << "Published event to " << successfulCount << "/" << targetCount << " target relays."; + + return make_tuple(successfulRelays, failedRelays); +}; + RelayList NostrService::getConnectedRelays(RelayList relays) { PLOG_VERBOSE << "Identifying connected relays."; @@ -245,4 +287,10 @@ void NostrService::disconnect(string relay) lock_guard lock(this->_propertyMutex); this->eraseActiveRelay(relay); }; + +string NostrService::generateSubscriptionId() +{ + uuid uuid = random_generator()(); + return to_string(uuid); +}; } // namespace nostr -- cgit From fea9005732607ee58a4bcb113b1805028954498a Mon Sep 17 00:00:00 2001 From: Michael Jurkoic Date: Sun, 17 Mar 2024 18:32:45 -0500 Subject: Add service methods to close filter subscriptions --- include/nostr.hpp | 32 ++++++++++++++++++++++++-- src/nostr_service.cpp | 64 ++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 91 insertions(+), 5 deletions(-) (limited to 'src/nostr_service.cpp') diff --git a/include/nostr.hpp b/include/nostr.hpp index 22d9956..c410046 100644 --- a/include/nostr.hpp +++ b/include/nostr.hpp @@ -151,13 +151,28 @@ public: std::tuple queryRelays(Filters filters); // TODO: Write a method that receives events for an active subscription. - // TODO: Write a method that closes active subscriptions. + + /** + * @brief Closes the subscription with the given ID on all open relay connections. + * @returns A tuple of `RelayList` objects, of the form ``, indicating + * to which relays the message was sent successfully, and which relays failed to receive the + * message. + */ + std::tuple closeSubscription(std::string subscriptionId); + + /** + * @brief Closes all open subscriptions on the given relays. + * @returns A tuple of `RelayList` objects, of the form ``, indicating + * to which relays the message was sent successfully, and which relays failed to receive the + * message. + */ + std::tuple closeSubscriptions(RelayList relays); private: std::mutex _propertyMutex; RelayList _defaultRelays; RelayList _activeRelays; - std::unordered_map> _subscriptionIds; + std::unordered_map> _subscriptions; client::IWebSocketClient* _client; /** @@ -198,5 +213,18 @@ private: * @returns A stringified UUID. */ std::string generateSubscriptionId(); + + /** + * @brief Generates a message requesting a relay to close the subscription with the given ID. + * @returns A stringified JSON object representing the close request. + */ + std::string generateCloseRequest(std::string subscriptionId); + + /** + * @brief Indicates whether the connection to the given relay has a subscription with the given + * ID. + * @returns True if the relay has the subscription, false otherwise. + */ + bool hasSubscription(std::string relay, std::string subscriptionId); }; } // namespace nostr diff --git a/src/nostr_service.cpp b/src/nostr_service.cpp index 3025b96..e3b1f19 100644 --- a/src/nostr_service.cpp +++ b/src/nostr_service.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -9,10 +10,11 @@ #include "nostr.hpp" #include "client/web_socket_client.hpp" -using std::async; using boost::uuids::random_generator; using boost::uuids::to_string; using boost::uuids::uuid; +using nlohmann::json; +using std::async; using std::future; using std::lock_guard; using std::make_tuple; @@ -154,7 +156,7 @@ tuple NostrService::queryRelays(Filters filters) for (const string relay : this->_activeRelays) { string subscriptionId = this->generateSubscriptionId(); - this->_subscriptionIds[relay].push_back(subscriptionId); + this->_subscriptions[relay].push_back(subscriptionId); string request = filters.serialize(subscriptionId); future> requestFuture = async([this, &relay, &request]() { @@ -178,7 +180,47 @@ tuple NostrService::queryRelays(Filters filters) size_t targetCount = this->_activeRelays.size(); size_t successfulCount = successfulRelays.size(); - PLOG_INFO << "Published event to " << successfulCount << "/" << targetCount << " target relays."; + PLOG_INFO << "Sent query to " << successfulCount << "/" << targetCount << " open relay connections."; + + return make_tuple(successfulRelays, failedRelays); +}; + +tuple NostrService::closeSubscription(string subscriptionId) +{ + RelayList successfulRelays; + RelayList failedRelays; + + vector>> closeFutures; + for (const string relay : this->_activeRelays) + { + if (!this->hasSubscription(relay, subscriptionId)) + { + continue; + } + + string request = this->generateCloseRequest(subscriptionId); + future> closeFuture = async([this, &relay, &request]() { + return this->_client->send(request, relay); + }); + closeFutures.push_back(move(closeFuture)); + } + + for (auto& closeFuture : closeFutures) + { + auto [relay, isSuccess] = closeFuture.get(); + if (isSuccess) + { + successfulRelays.push_back(relay); + } + else + { + failedRelays.push_back(relay); + } + } + + size_t targetCount = this->_activeRelays.size(); + size_t successfulCount = successfulRelays.size(); + PLOG_INFO << "Sent close request to " << successfulCount << "/" << targetCount << " open relay connections."; return make_tuple(successfulRelays, failedRelays); }; @@ -293,4 +335,20 @@ string NostrService::generateSubscriptionId() uuid uuid = random_generator()(); return to_string(uuid); }; + +string NostrService::generateCloseRequest(string subscriptionId) +{ + json jarr = json::array({ "CLOSE", subscriptionId }); + return jarr.dump(); +}; + +bool NostrService::hasSubscription(string relay, string subscriptionId) +{ + auto it = find(this->_subscriptions[relay].begin(), this->_subscriptions[relay].end(), subscriptionId); + if (it != this->_subscriptions[relay].end()) // If the subscription is in this->_subscriptions[relay] + { + return true; + } + return false; +}; } // namespace nostr -- cgit From c7096828e62fcea63120504b867150130377ab75 Mon Sep 17 00:00:00 2001 From: Michael Jurkoic Date: Sun, 17 Mar 2024 19:12:30 -0500 Subject: Provide methods to close all open subscriptions --- include/nostr.hpp | 8 ++++++++ src/nostr_service.cpp | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) (limited to 'src/nostr_service.cpp') diff --git a/include/nostr.hpp b/include/nostr.hpp index c410046..8a9d4c9 100644 --- a/include/nostr.hpp +++ b/include/nostr.hpp @@ -160,6 +160,14 @@ public: */ std::tuple closeSubscription(std::string subscriptionId); + /** + * @brief Closes all open subscriptions on all open relay connections. + * @returns A tuple of `RelayList` objects, of the form ``, indicating + * to which relays the message was sent successfully, and which relays failed to receive the + * message. + */ + std::tuple closeSubscriptions(); + /** * @brief Closes all open subscriptions on the given relays. * @returns A tuple of `RelayList` objects, of the form ``, indicating diff --git a/src/nostr_service.cpp b/src/nostr_service.cpp index e3b1f19..3ac5177 100644 --- a/src/nostr_service.cpp +++ b/src/nostr_service.cpp @@ -225,6 +225,49 @@ tuple NostrService::closeSubscription(string subscriptionI return make_tuple(successfulRelays, failedRelays); }; +tuple NostrService::closeSubscriptions() +{ + return this->closeSubscriptions(this->_activeRelays); +}; + +tuple NostrService::closeSubscriptions(RelayList relays) +{ + RelayList successfulRelays; + RelayList failedRelays; + + vector>> closeFutures; + for (const string relay : relays) + { + future> closeFuture = async([this, &relay]() { + RelayList successfulRelays; + RelayList failedRelays; + + for (const string& subscriptionId : this->_subscriptions[relay]) + { + auto [successes, failures] = this->closeSubscription(subscriptionId); + successfulRelays.insert(successfulRelays.end(), successes.begin(), successes.end()); + failedRelays.insert(failedRelays.end(), failures.begin(), failures.end()); + } + + return make_tuple(successfulRelays, failedRelays); + }); + closeFutures.push_back(move(closeFuture)); + } + + for (auto& closeFuture : closeFutures) + { + auto [successes, failures] = closeFuture.get(); + successfulRelays.insert(successfulRelays.end(), successes.begin(), successes.end()); + failedRelays.insert(failedRelays.end(), failures.begin(), failures.end()); + } + + size_t targetCount = relays.size(); + size_t successfulCount = successfulRelays.size(); + PLOG_INFO << "Sent close requests to " << successfulCount << "/" << targetCount << " open relay connections."; + + return make_tuple(successfulRelays, failedRelays); +}; + RelayList NostrService::getConnectedRelays(RelayList relays) { PLOG_VERBOSE << "Identifying connected relays."; -- cgit From 299a2567430dd96800d6b3ca81a3d198be4a18fd Mon Sep 17 00:00:00 2001 From: Michael Jurkoic Date: Sun, 17 Mar 2024 19:30:15 -0500 Subject: Begin defining relay message handling --- include/client/web_socket_client.hpp | 6 ++--- include/nostr.hpp | 44 +++++++++++++++++++++++++++++++----- src/nostr_service.cpp | 26 ++++++++++++++++++--- test/nostr_service_test.cpp | 2 +- 4 files changed, 65 insertions(+), 13 deletions(-) (limited to 'src/nostr_service.cpp') diff --git a/include/client/web_socket_client.hpp b/include/client/web_socket_client.hpp index 4e6cb8b..f676e59 100644 --- a/include/client/web_socket_client.hpp +++ b/include/client/web_socket_client.hpp @@ -45,10 +45,10 @@ public: /** * @brief Sets up a message handler for the given server. * @param uri The URI of the server to which the message handler should be attached. - * @param messageHandler A callable object that will be invoked when the client receives a - * message from the server. + * @param messageHandler A callable object that will be invoked with the subscription ID and + * the message contents when the client receives a message from the server. */ - virtual void receive(std::string uri, std::function messageHandler) = 0; + virtual void receive(std::string uri, std::function messageHandler) = 0; /** * @brief Closes the connection to the given server. diff --git a/include/nostr.hpp b/include/nostr.hpp index 8a9d4c9..448ad64 100644 --- a/include/nostr.hpp +++ b/include/nostr.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -144,13 +145,33 @@ public: /** * @brief Queries all open relay connections for events matching the given set of filters. - * @returns A tuple of `RelayList` objects, of the form ``, indicating - * to which relays the request was successfully sent, and which relays did not successfully - * receive the request. + * @param filters The filters to use for the query. + * @returns The ID of the subscription created for the query. + */ + std::string queryRelays(Filters filters); + + /** + * @brief Queries all open relay connections for events matching the given set of filters. + * @param filters The filters to use for the query. + * @param responseHandler A callable object that will be invoked each time the client receives + * an event matching the filters. + * @returns The ID of the subscription created for the query. + */ + std::string queryRelays(Filters filters, std::function responseHandler); + + /** + * @brief Get any new events received since the last call to this method, across all + * subscriptions. + * @returns A pointer to a vector of new events. */ - std::tuple queryRelays(Filters filters); + std::unique_ptr> getNewEvents(); - // TODO: Write a method that receives events for an active subscription. + /** + * @brief Get any new events received since the last call to this method, for the given + * subscription. + * @returns A pointer to a vector of new events. + */ + std::unique_ptr> getNewEvents(std::string subscriptionId); /** * @brief Closes the subscription with the given ID on all open relay connections. @@ -177,11 +198,13 @@ public: std::tuple closeSubscriptions(RelayList relays); private: + client::IWebSocketClient* _client; std::mutex _propertyMutex; RelayList _defaultRelays; RelayList _activeRelays; std::unordered_map> _subscriptions; - client::IWebSocketClient* _client; + std::unordered_map> _events; + std::unordered_map::iterator> _eventIterators; /** * @brief Determines which of the given relays are currently connected. @@ -234,5 +257,14 @@ private: * @returns True if the relay has the subscription, false otherwise. */ bool hasSubscription(std::string relay, std::string subscriptionId); + + /** + * @brief A default message handler for events returned from relay queries. + * @param subscriptionId The ID of the subscription for which the event was received. + * @param event The event received from the relay. + * @remark By default, new events are stored in a map of subscription IDs to vectors of events. + * Events are retrieved by calling `getNewEvents` or `getNewEvents(subscriptionId)`. + */ + void onEvent(std::string subscriptionId, Event event); }; } // namespace nostr diff --git a/src/nostr_service.cpp b/src/nostr_service.cpp index 3ac5177..13d5ff5 100644 --- a/src/nostr_service.cpp +++ b/src/nostr_service.cpp @@ -15,6 +15,7 @@ using boost::uuids::to_string; using boost::uuids::uuid; using nlohmann::json; using std::async; +using std::function; using std::future; using std::lock_guard; using std::make_tuple; @@ -147,15 +148,22 @@ tuple NostrService::publishEvent(Event event) return make_tuple(successfulRelays, failedRelays); }; -tuple NostrService::queryRelays(Filters filters) +string NostrService::queryRelays(Filters filters) +{ + return this->queryRelays(filters, [this](string subscriptionId, Event event) { + this->onEvent(subscriptionId, event); + }); +}; + +string NostrService::queryRelays(Filters filters, function responseHandler) { RelayList successfulRelays; RelayList failedRelays; + string subscriptionId = this->generateSubscriptionId(); vector>> requestFutures; for (const string relay : this->_activeRelays) { - string subscriptionId = this->generateSubscriptionId(); this->_subscriptions[relay].push_back(subscriptionId); string request = filters.serialize(subscriptionId); @@ -163,6 +171,12 @@ tuple NostrService::queryRelays(Filters filters) return this->_client->send(request, relay); }); requestFutures.push_back(move(requestFuture)); + + this->_client->receive(relay, [responseHandler](string subscriptionId, string message) { + Event event; + event.deserialize(message); + responseHandler(subscriptionId, event); + }); } for (auto& publishFuture : requestFutures) @@ -182,7 +196,7 @@ tuple NostrService::queryRelays(Filters filters) size_t successfulCount = successfulRelays.size(); PLOG_INFO << "Sent query to " << successfulCount << "/" << targetCount << " open relay connections."; - return make_tuple(successfulRelays, failedRelays); + return subscriptionId; }; tuple NostrService::closeSubscription(string subscriptionId) @@ -394,4 +408,10 @@ bool NostrService::hasSubscription(string relay, string subscriptionId) } return false; }; + +void NostrService::onEvent(string subscriptionId, Event event) +{ + _events[subscriptionId].push_back(event); + PLOG_INFO << "Received event for subscription: " << subscriptionId; +}; } // namespace nostr diff --git a/test/nostr_service_test.cpp b/test/nostr_service_test.cpp index d4fc71b..2dd34d2 100644 --- a/test/nostr_service_test.cpp +++ b/test/nostr_service_test.cpp @@ -28,7 +28,7 @@ public: MOCK_METHOD(void, openConnection, (string uri), (override)); MOCK_METHOD(bool, isConnected, (string uri), (override)); MOCK_METHOD((tuple), send, (string message, string uri), (override)); - MOCK_METHOD(void, receive, (string uri, function messageHandler), (override)); + MOCK_METHOD(void, receive, (string uri, function messageHandler), (override)); MOCK_METHOD(void, closeConnection, (string uri), (override)); }; -- cgit From aaba3db6976f9bb8e92ae7ff1075f9719f8936c1 Mon Sep 17 00:00:00 2001 From: Michael Jurkoic Date: Mon, 18 Mar 2024 20:00:17 -0500 Subject: Provide option to store events for async retrieval --- include/nostr.hpp | 21 ++++++++++++++++--- src/nostr_service.cpp | 56 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 3 deletions(-) (limited to 'src/nostr_service.cpp') diff --git a/include/nostr.hpp b/include/nostr.hpp index 448ad64..1a4e33c 100644 --- a/include/nostr.hpp +++ b/include/nostr.hpp @@ -147,6 +147,8 @@ public: * @brief Queries all open relay connections for events matching the given set of filters. * @param filters The filters to use for the query. * @returns The ID of the subscription created for the query. + * @remarks The service will store a limited number of events returned from the relay for the + * given filters. These events may be retrieved via `getNewEvents`. */ std::string queryRelays(Filters filters); @@ -156,6 +158,9 @@ public: * @param responseHandler A callable object that will be invoked each time the client receives * an event matching the filters. * @returns The ID of the subscription created for the query. + * @remark By providing a response handler, the caller assumes responsibility for handling all + * events returned from the relay for the given filters. The service will not store the + * events, and they will not be accessible via `getNewEvents`. */ std::string queryRelays(Filters filters, std::function responseHandler); @@ -164,14 +169,14 @@ public: * subscriptions. * @returns A pointer to a vector of new events. */ - std::unique_ptr> getNewEvents(); + std::vector getNewEvents(); /** * @brief Get any new events received since the last call to this method, for the given * subscription. * @returns A pointer to a vector of new events. */ - std::unique_ptr> getNewEvents(std::string subscriptionId); + std::vector getNewEvents(std::string subscriptionId); /** * @brief Closes the subscription with the given ID on all open relay connections. @@ -198,12 +203,22 @@ public: std::tuple closeSubscriptions(RelayList relays); private: + ///< The maximum number of events the service will store for each subscription. + const int MAX_EVENTS_PER_SUBSCRIPTION = 128; + + ///< The WebSocket client used to communicate with relays. client::IWebSocketClient* _client; + ///< A mutex to protect the instance properties. std::mutex _propertyMutex; + ///< The default set of Nostr relays to which the service will attempt to connect. RelayList _defaultRelays; - RelayList _activeRelays; + ///< The set of Nostr relays to which the service is currently connected. + RelayList _activeRelays; + ///< A map from relay URIs to the subscription IDs open on each relay. std::unordered_map> _subscriptions; + ///< A map from subscription IDs to the events returned by the relays for each subscription. std::unordered_map> _events; + ///< A map from the subscription IDs to the latest read event for each subscription. std::unordered_map::iterator> _eventIterators; /** diff --git a/src/nostr_service.cpp b/src/nostr_service.cpp index 13d5ff5..50609b4 100644 --- a/src/nostr_service.cpp +++ b/src/nostr_service.cpp @@ -21,6 +21,7 @@ using std::lock_guard; using std::make_tuple; using std::move; using std::mutex; +using std::out_of_range; using std::string; using std::thread; using std::tuple; @@ -151,6 +152,7 @@ tuple NostrService::publishEvent(Event event) string NostrService::queryRelays(Filters filters) { return this->queryRelays(filters, [this](string subscriptionId, Event event) { + this->_eventIterators[subscriptionId] = this->_events[subscriptionId].begin(); this->onEvent(subscriptionId, event); }); }; @@ -199,6 +201,46 @@ string NostrService::queryRelays(Filters filters, function return subscriptionId; }; +vector NostrService::getNewEvents() +{ + vector newEvents; + + for (auto& [subscriptionId, events] : this->_events) + { + vector subscriptionEvents = this->getNewEvents(subscriptionId); + newEvents.insert(newEvents.end(), subscriptionEvents.begin(), subscriptionEvents.end()); + } + + return newEvents; +}; + +vector NostrService::getNewEvents(string subscriptionId) +{ + if (this->_events.find(subscriptionId) == this->_events.end()) + { + PLOG_ERROR << "No events found for subscription: " << subscriptionId; + throw out_of_range("No events found for subscription: " + subscriptionId); + } + + if (this->_eventIterators.find(subscriptionId) == this->_eventIterators.end()) + { + PLOG_ERROR << "No event iterator found for subscription: " << subscriptionId; + throw out_of_range("No event iterator found for subscription: " + subscriptionId); + } + + vector newEvents; + vector receivedEvents = this->_events[subscriptionId]; + vector::iterator eventIt = this->_eventIterators[subscriptionId]; + + while (eventIt != receivedEvents.end()) + { + newEvents.push_back(move(*eventIt)); + eventIt++; + } + + return newEvents; +}; + tuple NostrService::closeSubscription(string subscriptionId) { RelayList successfulRelays; @@ -413,5 +455,19 @@ void NostrService::onEvent(string subscriptionId, Event event) { _events[subscriptionId].push_back(event); PLOG_INFO << "Received event for subscription: " << subscriptionId; + + // To protect memory, only keep a limited number of events per subscription. + while (_events[subscriptionId].size() > NostrService::MAX_EVENTS_PER_SUBSCRIPTION) + { + auto startIt = _events[subscriptionId].begin(); + auto eventIt = _eventIterators[subscriptionId]; + + if (eventIt == startIt) + { + eventIt++; + } + + _events[subscriptionId].erase(_events[subscriptionId].begin()); + } }; } // namespace nostr -- cgit From b766baf6f34df321e8eff9687cc2c17485da6fb4 Mon Sep 17 00:00:00 2001 From: Michael Jurkoic Date: Mon, 18 Mar 2024 20:20:36 -0500 Subject: Use smart pointers --- include/nostr.hpp | 11 ++++++++--- src/nostr_service.cpp | 24 +++++++++++++++++------- test/nostr_service_test.cpp | 24 ++++++++++++------------ 3 files changed, 37 insertions(+), 22 deletions(-) (limited to 'src/nostr_service.cpp') diff --git a/include/nostr.hpp b/include/nostr.hpp index 1a4e33c..51ea7cd 100644 --- a/include/nostr.hpp +++ b/include/nostr.hpp @@ -104,8 +104,13 @@ private: class NostrService { public: - NostrService(plog::IAppender* appender, client::IWebSocketClient* client); - NostrService(plog::IAppender* appender, client::IWebSocketClient* client, RelayList relays); + NostrService( + std::shared_ptr appender, + std::shared_ptr client); + NostrService( + std::shared_ptr appender, + std::shared_ptr client, + RelayList relays); ~NostrService(); RelayList defaultRelays() const; @@ -207,7 +212,7 @@ private: const int MAX_EVENTS_PER_SUBSCRIPTION = 128; ///< The WebSocket client used to communicate with relays. - client::IWebSocketClient* _client; + shared_ptr _client; ///< A mutex to protect the instance properties. std::mutex _propertyMutex; ///< The default set of Nostr relays to which the service will attempt to connect. diff --git a/src/nostr_service.cpp b/src/nostr_service.cpp index 50609b4..0409a0d 100644 --- a/src/nostr_service.cpp +++ b/src/nostr_service.cpp @@ -22,27 +22,33 @@ using std::make_tuple; using std::move; using std::mutex; using std::out_of_range; +using std::shared_ptr; using std::string; using std::thread; using std::tuple; +using std::unique_ptr; using std::vector; namespace nostr { -NostrService::NostrService(plog::IAppender* appender, client::IWebSocketClient* client) - : NostrService(appender, client, {}) { }; - -NostrService::NostrService(plog::IAppender* appender, client::IWebSocketClient* client, RelayList relays) - : _defaultRelays(relays), _client(client) +NostrService::NostrService( + shared_ptr appender, + shared_ptr client) +: NostrService(appender, client, {}) { }; + +NostrService::NostrService( + shared_ptr appender, + shared_ptr client, + RelayList relays) +: _defaultRelays(relays), _client(client) { - plog::init(plog::debug, appender); + plog::init(plog::debug, appender.get()); client->start(); }; NostrService::~NostrService() { this->_client->stop(); - delete this->_client; }; RelayList NostrService::defaultRelays() const { return this->_defaultRelays; }; @@ -152,6 +158,7 @@ tuple NostrService::publishEvent(Event event) string NostrService::queryRelays(Filters filters) { return this->queryRelays(filters, [this](string subscriptionId, Event event) { + lock_guard lock(this->_propertyMutex); this->_eventIterators[subscriptionId] = this->_events[subscriptionId].begin(); this->onEvent(subscriptionId, event); }); @@ -166,6 +173,7 @@ string NostrService::queryRelays(Filters filters, function vector>> requestFutures; for (const string relay : this->_activeRelays) { + lock_guard lock(this->_propertyMutex); this->_subscriptions[relay].push_back(subscriptionId); string request = filters.serialize(subscriptionId); @@ -228,6 +236,7 @@ vector NostrService::getNewEvents(string subscriptionId) throw out_of_range("No event iterator found for subscription: " + subscriptionId); } + lock_guard lock(this->_propertyMutex); vector newEvents; vector receivedEvents = this->_events[subscriptionId]; vector::iterator eventIt = this->_eventIterators[subscriptionId]; @@ -453,6 +462,7 @@ bool NostrService::hasSubscription(string relay, string subscriptionId) void NostrService::onEvent(string subscriptionId, Event event) { + lock_guard lock(this->_propertyMutex); _events[subscriptionId].push_back(event); PLOG_INFO << "Received event for subscription: " << subscriptionId; diff --git a/test/nostr_service_test.cpp b/test/nostr_service_test.cpp index 2dd34d2..70f4d9e 100644 --- a/test/nostr_service_test.cpp +++ b/test/nostr_service_test.cpp @@ -56,12 +56,12 @@ TEST_F(NostrServiceTest, Constructor_StartsClient) { EXPECT_CALL(*testClient, start()).Times(1); - auto nostrService = new nostr::NostrService(testAppender.get(), testClient.get()); + auto nostrService = new nostr::NostrService(testAppender, testClient); }; TEST_F(NostrServiceTest, Constructor_InitializesService_WithNoDefaultRelays) { - auto nostrService = new nostr::NostrService(testAppender.get(), testClient.get()); + auto nostrService = new nostr::NostrService(testAppender, testClient); auto defaultRelays = nostrService->defaultRelays(); auto activeRelays = nostrService->activeRelays(); @@ -71,7 +71,7 @@ TEST_F(NostrServiceTest, Constructor_InitializesService_WithNoDefaultRelays) TEST_F(NostrServiceTest, Constructor_InitializesService_WithProvidedDefaultRelays) { - auto nostrService = new nostr::NostrService(testAppender.get(), testClient.get(), defaultTestRelays); + auto nostrService = new nostr::NostrService(testAppender, testClient, defaultTestRelays); auto defaultRelays = nostrService->defaultRelays(); auto activeRelays = nostrService->activeRelays(); @@ -87,7 +87,7 @@ TEST_F(NostrServiceTest, Destructor_StopsClient) { EXPECT_CALL(*testClient, start()).Times(1); - auto nostrService = new nostr::NostrService(testAppender.get(), testClient.get()); + auto nostrService = new nostr::NostrService(testAppender, testClient); }; TEST_F(NostrServiceTest, OpenRelayConnections_OpensConnections_ToDefaultRelays) @@ -112,7 +112,7 @@ TEST_F(NostrServiceTest, OpenRelayConnections_OpensConnections_ToDefaultRelays) return status; })); - auto nostrService = new nostr::NostrService(testAppender.get(), testClient.get(), defaultTestRelays); + auto nostrService = new nostr::NostrService(testAppender, testClient, defaultTestRelays); nostrService->openRelayConnections(); auto activeRelays = nostrService->activeRelays(); @@ -147,7 +147,7 @@ TEST_F(NostrServiceTest, OpenRelayConnections_OpensConnections_ToProvidedRelays) return status; })); - auto nostrService = new nostr::NostrService(testAppender.get(), testClient.get(), defaultTestRelays); + auto nostrService = new nostr::NostrService(testAppender, testClient, defaultTestRelays); nostrService->openRelayConnections(testRelays); auto activeRelays = nostrService->activeRelays(); @@ -184,7 +184,7 @@ TEST_F(NostrServiceTest, OpenRelayConnections_AddsOpenConnections_ToActiveRelays return status; })); - auto nostrService = new nostr::NostrService(testAppender.get(), testClient.get(), defaultTestRelays); + auto nostrService = new nostr::NostrService(testAppender, testClient, defaultTestRelays); nostrService->openRelayConnections(); auto activeRelays = nostrService->activeRelays(); @@ -227,7 +227,7 @@ TEST_F(NostrServiceTest, CloseRelayConnections_ClosesConnections_ToActiveRelays) return status; })); - auto nostrService = new nostr::NostrService(testAppender.get(), testClient.get(), defaultTestRelays); + auto nostrService = new nostr::NostrService(testAppender, testClient, defaultTestRelays); nostrService->openRelayConnections(); EXPECT_CALL(*testClient, closeConnection(defaultTestRelays[0])).Times(1); @@ -262,7 +262,7 @@ TEST_F(NostrServiceTest, CloseRelayConnections_RemovesClosedConnections_FromActi return status; })); - auto nostrService = new nostr::NostrService(testAppender.get(), testClient.get(), allTestRelays); + auto nostrService = new nostr::NostrService(testAppender, testClient, allTestRelays); nostrService->openRelayConnections(); EXPECT_CALL(*testClient, closeConnection(testRelays[0])).Times(1); @@ -300,7 +300,7 @@ TEST_F(NostrServiceTest, PublishEvent_CorrectlyIndicates_AllSuccesses) return status; })); - auto nostrService = new nostr::NostrService(testAppender.get(), testClient.get(), defaultTestRelays); + auto nostrService = new nostr::NostrService(testAppender, testClient, defaultTestRelays); nostrService->openRelayConnections(); EXPECT_CALL(*testClient, send(_, _)) @@ -340,7 +340,7 @@ TEST_F(NostrServiceTest, PublishEvent_CorrectlyIndicates_AllFailures) return status; })); - auto nostrService = new nostr::NostrService(testAppender.get(), testClient.get(), defaultTestRelays); + auto nostrService = new nostr::NostrService(testAppender, testClient, defaultTestRelays); nostrService->openRelayConnections(); EXPECT_CALL(*testClient, send(_, _)) @@ -380,7 +380,7 @@ TEST_F(NostrServiceTest, PublishEvent_CorrectlyIndicates_MixedSuccessesAndFailur return status; })); - auto nostrService = new nostr::NostrService(testAppender.get(), testClient.get(), defaultTestRelays); + auto nostrService = new nostr::NostrService(testAppender, testClient, defaultTestRelays); nostrService->openRelayConnections(); EXPECT_CALL(*testClient, send(_, defaultTestRelays[0])) -- cgit From 6dde23e6c66e846c64d49c5258f0dbf44e3d0374 Mon Sep 17 00:00:00 2001 From: Michael Jurkoic Date: Mon, 18 Mar 2024 21:28:19 -0500 Subject: Declare a signer interface --- include/nostr.hpp | 16 ++++++++++++++-- src/event.cpp | 6 +++--- src/nostr_service.cpp | 2 +- 3 files changed, 18 insertions(+), 6 deletions(-) (limited to 'src/nostr_service.cpp') diff --git a/include/nostr.hpp b/include/nostr.hpp index 645090a..3e60d7b 100644 --- a/include/nostr.hpp +++ b/include/nostr.hpp @@ -18,7 +18,9 @@ namespace nostr typedef std::vector RelayList; typedef std::unordered_map> TagMap; -// TODO: Add null checking to seralization and deserialization methods. +class ISigner; +class NostrService; + /** * @brief A Nostr event. * @remark All data transmitted over the Nostr protocol is encoded in JSON blobs. This struct @@ -40,7 +42,7 @@ struct Event * @returns A stringified JSON object representing the event. * @throws `std::invalid_argument` if the event object is invalid. */ - std::string serialize(); + std::string serialize(std::shared_ptr signer); /** * @brief Deserializes the event from a JSON string. @@ -103,6 +105,7 @@ private: class NostrService { +// TODO: Setup signer in the constructor. public: NostrService( std::shared_ptr appender, @@ -213,6 +216,9 @@ private: ///< The WebSocket client used to communicate with relays. std::shared_ptr _client; + ///< The signer used to sign Nostr events. + std::shared_ptr _signer; + ///< A mutex to protect the instance properties. std::mutex _propertyMutex; ///< The default set of Nostr relays to which the service will attempt to connect. @@ -287,4 +293,10 @@ private: */ void onEvent(std::string subscriptionId, Event event); }; + +class ISigner +{ +public: + virtual std::string generateSignature(std::shared_ptr event) = 0; +}; } // namespace nostr diff --git a/src/event.cpp b/src/event.cpp index 4ba87d2..a24a594 100644 --- a/src/event.cpp +++ b/src/event.cpp @@ -13,13 +13,14 @@ using std::hex; using std::invalid_argument; using std::setw; using std::setfill; +using std::shared_ptr; using std::string; using std::stringstream; using std::time; namespace nostr { -string Event::serialize() +string Event::serialize(shared_ptr signer) { try { @@ -40,8 +41,7 @@ string Event::serialize() }; j["id"] = this->generateId(j.dump()); - - // TODO: Reach out to a signer to sign the event, then set the signature. + j["sig"] = signer->generateSignature(shared_ptr(this)); json jarr = json::array({ "EVENT", j }); diff --git a/src/nostr_service.cpp b/src/nostr_service.cpp index 0409a0d..7efc11e 100644 --- a/src/nostr_service.cpp +++ b/src/nostr_service.cpp @@ -129,7 +129,7 @@ tuple NostrService::publishEvent(Event event) for (const string& relay : this->_activeRelays) { future> publishFuture = async([this, &relay, &event]() { - return this->_client->send(event.serialize(), relay); + return this->_client->send(event.serialize(this->_signer), relay); }); publishFutures.push_back(move(publishFuture)); -- cgit From 8dbce9cd5aab9129e66a0c04e31467d172344f19 Mon Sep 17 00:00:00 2001 From: Michael Jurkoic Date: Tue, 19 Mar 2024 09:12:00 -0500 Subject: Move relay payload parsing into NostrService Preserve separation of concerns. --- include/client/web_socket_client.hpp | 6 +++--- include/nostr.hpp | 7 ++++++- src/client/websocketpp_client.cpp | 25 +++++++------------------ src/nostr_service.cpp | 27 ++++++++++++++++++++++----- test/nostr_service_test.cpp | 2 +- 5 files changed, 39 insertions(+), 28 deletions(-) (limited to 'src/nostr_service.cpp') diff --git a/include/client/web_socket_client.hpp b/include/client/web_socket_client.hpp index f676e59..3ef2b86 100644 --- a/include/client/web_socket_client.hpp +++ b/include/client/web_socket_client.hpp @@ -45,10 +45,10 @@ public: /** * @brief Sets up a message handler for the given server. * @param uri The URI of the server to which the message handler should be attached. - * @param messageHandler A callable object that will be invoked with the subscription ID and - * the message contents when the client receives a message from the server. + * @param messageHandler A callable object that will be invoked with the payload the client + * receives from the server. */ - virtual void receive(std::string uri, std::function messageHandler) = 0; + virtual void receive(std::string uri, std::function messageHandler) = 0; /** * @brief Closes the connection to the given server. diff --git a/include/nostr.hpp b/include/nostr.hpp index 3e60d7b..2b04862 100644 --- a/include/nostr.hpp +++ b/include/nostr.hpp @@ -170,7 +170,7 @@ public: * events returned from the relay for the given filters. The service will not store the * events, and they will not be accessible via `getNewEvents`. */ - std::string queryRelays(Filters filters, std::function responseHandler); + std::string queryRelays(Filters filters, std::function responseHandler); /** * @brief Get any new events received since the last call to this method, across all @@ -284,6 +284,11 @@ private: */ bool hasSubscription(std::string relay, std::string subscriptionId); + /** + * @brief Parses messages received from the relay and invokes the appropriate message handler. + */ + void onMessage(std::string message, std::function eventHandler); + /** * @brief A default message handler for events returned from relay queries. * @param subscriptionId The ID of the subscription for which the event was received. diff --git a/src/client/websocketpp_client.cpp b/src/client/websocketpp_client.cpp index 5199343..981d4ec 100644 --- a/src/client/websocketpp_client.cpp +++ b/src/client/websocketpp_client.cpp @@ -1,10 +1,8 @@ -#include #include #include #include "web_socket_client.hpp" -using nlohmann::json; using std::error_code; using std::function; using std::lock_guard; @@ -86,26 +84,17 @@ public: return make_tuple(uri, true); }; - void receive(string uri, function messageHandler) override + void receive(string uri, function messageHandler) override { - this->_client.set_message_handler([this, messageHandler]( + lock_guard lock(this->_propertyMutex); + auto connectionHandle = this->_connectionHandles[uri]; + auto connection = this->_client.get_con_from_hdl(connectionHandle); + + connection->set_message_handler([messageHandler]( websocketpp::connection_hdl connectionHandle, websocketpp_client::message_ptr message) { - json jarr = json::array(); - string payload = message->get_payload(); - - jarr.parse(payload); - string messageType = jarr[0]; - - if (messageType == "EVENT") - { - string subscriptionId = jarr[1]; - string messageContents = jarr[2].dump(); - messageHandler(subscriptionId, messageContents); - }; - - // TODO: Add support for other message types. + messageHandler(message->get_payload()); }); }; diff --git a/src/nostr_service.cpp b/src/nostr_service.cpp index 7efc11e..ac63f23 100644 --- a/src/nostr_service.cpp +++ b/src/nostr_service.cpp @@ -164,7 +164,7 @@ string NostrService::queryRelays(Filters filters) }); }; -string NostrService::queryRelays(Filters filters, function responseHandler) +string NostrService::queryRelays(Filters filters, function responseHandler) { RelayList successfulRelays; RelayList failedRelays; @@ -182,10 +182,8 @@ string NostrService::queryRelays(Filters filters, function }); requestFutures.push_back(move(requestFuture)); - this->_client->receive(relay, [responseHandler](string subscriptionId, string message) { - Event event; - event.deserialize(message); - responseHandler(subscriptionId, event); + this->_client->receive(relay, [this, responseHandler](string payload) { + this->onMessage(payload, responseHandler); }); } @@ -460,6 +458,25 @@ bool NostrService::hasSubscription(string relay, string subscriptionId) return false; }; +void NostrService::onMessage(string message, function eventHandler) +{ + json jarr = json::array(); + jarr = json::parse(message); + + string messageType = jarr[0]; + + if (messageType == "EVENT") + { + string subscriptionId = jarr[1]; + string serializedEvent = jarr[2].dump(); + Event event; + event.deserialize(message); + eventHandler(subscriptionId, event); + } + + // Support other message types here, if necessary. +}; + void NostrService::onEvent(string subscriptionId, Event event) { lock_guard lock(this->_propertyMutex); diff --git a/test/nostr_service_test.cpp b/test/nostr_service_test.cpp index 70f4d9e..1679ac5 100644 --- a/test/nostr_service_test.cpp +++ b/test/nostr_service_test.cpp @@ -28,7 +28,7 @@ public: MOCK_METHOD(void, openConnection, (string uri), (override)); MOCK_METHOD(bool, isConnected, (string uri), (override)); MOCK_METHOD((tuple), send, (string message, string uri), (override)); - MOCK_METHOD(void, receive, (string uri, function messageHandler), (override)); + MOCK_METHOD(void, receive, (string uri, function messageHandler), (override)); MOCK_METHOD(void, closeConnection, (string uri), (override)); }; -- cgit From 111b9914c601730a3697a3b7ff8a60fd2c15a38a Mon Sep 17 00:00:00 2001 From: Michael Jurkoic Date: Sat, 23 Mar 2024 11:38:29 -0500 Subject: Get smarter with pointers so tests pass --- include/nostr.hpp | 20 +++++--- src/event.cpp | 8 +-- src/nostr_service.cpp | 64 +++++++++++++++++------- test/nostr_service_test.cpp | 115 ++++++++++++++++++++++++++++---------------- 4 files changed, 136 insertions(+), 71 deletions(-) (limited to 'src/nostr_service.cpp') diff --git a/include/nostr.hpp b/include/nostr.hpp index 2b04862..2f37c51 100644 --- a/include/nostr.hpp +++ b/include/nostr.hpp @@ -42,7 +42,7 @@ struct Event * @returns A stringified JSON object representing the event. * @throws `std::invalid_argument` if the event object is invalid. */ - std::string serialize(std::shared_ptr signer); + std::string serialize(); /** * @brief Deserializes the event from a JSON string. @@ -105,14 +105,15 @@ private: class NostrService { -// TODO: Setup signer in the constructor. public: NostrService( std::shared_ptr appender, - std::shared_ptr client); + std::shared_ptr client, + std::shared_ptr signer); NostrService( std::shared_ptr appender, std::shared_ptr client, + std::shared_ptr signer, RelayList relays); ~NostrService(); @@ -149,7 +150,7 @@ public: * to which relays the event was published successfully, and to which relays the event failed * to publish. */ - std::tuple publishEvent(Event event); + std::tuple publishEvent(std::shared_ptr event); /** * @brief Queries all open relay connections for events matching the given set of filters. @@ -229,8 +230,8 @@ private: std::unordered_map> _subscriptions; ///< A map from subscription IDs to the events returned by the relays for each subscription. std::unordered_map> _events; - ///< A map from the subscription IDs to the latest read event for each subscription. - std::unordered_map::iterator> _eventIterators; + ///< A map from the subscription IDs to the ID of the latest read event for each subscription. + std::unordered_map _lastRead; /** * @brief Determines which of the given relays are currently connected. @@ -302,6 +303,11 @@ private: class ISigner { public: - virtual std::string generateSignature(std::shared_ptr event) = 0; + /** + * @brief Signs the given Nostr event. + * @param event The event to sign. + * @remark The event's `sig` field will be updated in-place with the signature. + */ + virtual void sign(std::shared_ptr event) = 0; }; } // namespace nostr diff --git a/src/event.cpp b/src/event.cpp index a24a594..e77e33d 100644 --- a/src/event.cpp +++ b/src/event.cpp @@ -11,6 +11,7 @@ using nlohmann::json; using std::hex; using std::invalid_argument; +using std::make_shared; using std::setw; using std::setfill; using std::shared_ptr; @@ -20,7 +21,7 @@ using std::time; namespace nostr { -string Event::serialize(shared_ptr signer) +string Event::serialize() { try { @@ -41,7 +42,6 @@ string Event::serialize(shared_ptr signer) }; j["id"] = this->generateId(j.dump()); - j["sig"] = signer->generateSignature(shared_ptr(this)); json jarr = json::array({ "EVENT", j }); @@ -80,8 +80,8 @@ void Event::validate() throw std::invalid_argument("Event::validate: A valid event kind is required."); } - bool hasSig = this->sig.length() > 0; - if (!hasSig) + bool hasSignature = this->sig.length() > 0; + if (!hasSignature) { throw std::invalid_argument("Event::validate: The event must be signed."); } diff --git a/src/nostr_service.cpp b/src/nostr_service.cpp index ac63f23..971516f 100644 --- a/src/nostr_service.cpp +++ b/src/nostr_service.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -15,6 +16,7 @@ using boost::uuids::to_string; using boost::uuids::uuid; using nlohmann::json; using std::async; +using std::find_if; using std::function; using std::future; using std::lock_guard; @@ -33,14 +35,16 @@ namespace nostr { NostrService::NostrService( shared_ptr appender, - shared_ptr client) -: NostrService(appender, client, {}) { }; + shared_ptr client, + shared_ptr signer) +: NostrService(appender, client, signer, {}) { }; NostrService::NostrService( shared_ptr appender, shared_ptr client, + shared_ptr signer, RelayList relays) -: _defaultRelays(relays), _client(client) +: _defaultRelays(relays), _client(client), _signer(signer) { plog::init(plog::debug, appender.get()); client->start(); @@ -118,20 +122,33 @@ void NostrService::closeRelayConnections(RelayList relays) } }; -tuple NostrService::publishEvent(Event event) +tuple NostrService::publishEvent(shared_ptr event) { RelayList successfulRelays; RelayList failedRelays; PLOG_INFO << "Attempting to publish event to Nostr relays."; + string serializedEvent; + try + { + this->_signer->sign(event); + serializedEvent = event->serialize(); + } + catch (const std::invalid_argument& error) + { + PLOG_ERROR << "Failed to sign event: " << error.what(); + throw error; + } + + lock_guard lock(this->_propertyMutex); vector>> publishFutures; for (const string& relay : this->_activeRelays) { - future> publishFuture = async([this, &relay, &event]() { - return this->_client->send(event.serialize(this->_signer), relay); + PLOG_INFO << "Entering lambda."; + future> publishFuture = async([this, relay, serializedEvent]() { + return this->_client->send(serializedEvent, relay); }); - publishFutures.push_back(move(publishFuture)); } @@ -159,7 +176,7 @@ string NostrService::queryRelays(Filters filters) { return this->queryRelays(filters, [this](string subscriptionId, Event event) { lock_guard lock(this->_propertyMutex); - this->_eventIterators[subscriptionId] = this->_events[subscriptionId].begin(); + this->_lastRead[subscriptionId] = event.id; this->onEvent(subscriptionId, event); }); }; @@ -228,16 +245,21 @@ vector NostrService::getNewEvents(string subscriptionId) throw out_of_range("No events found for subscription: " + subscriptionId); } - if (this->_eventIterators.find(subscriptionId) == this->_eventIterators.end()) + if (this->_lastRead.find(subscriptionId) == this->_lastRead.end()) { - PLOG_ERROR << "No event iterator found for subscription: " << subscriptionId; - throw out_of_range("No event iterator found for subscription: " + subscriptionId); + PLOG_ERROR << "No last read event ID found for subscription: " << subscriptionId; + throw out_of_range("No last read event ID found for subscription: " + subscriptionId); } lock_guard lock(this->_propertyMutex); vector newEvents; vector receivedEvents = this->_events[subscriptionId]; - vector::iterator eventIt = this->_eventIterators[subscriptionId]; + vector::iterator eventIt = find_if( + receivedEvents.begin(), + receivedEvents.end(), + [this,subscriptionId](Event event) { + return event.id == this->_lastRead[subscriptionId]; + }) + 1; while (eventIt != receivedEvents.end()) { @@ -480,20 +502,26 @@ void NostrService::onMessage(string message, function lock(this->_propertyMutex); - _events[subscriptionId].push_back(event); + this->_events[subscriptionId].push_back(event); PLOG_INFO << "Received event for subscription: " << subscriptionId; // To protect memory, only keep a limited number of events per subscription. - while (_events[subscriptionId].size() > NostrService::MAX_EVENTS_PER_SUBSCRIPTION) + while (this->_events[subscriptionId].size() > NostrService::MAX_EVENTS_PER_SUBSCRIPTION) { - auto startIt = _events[subscriptionId].begin(); - auto eventIt = _eventIterators[subscriptionId]; - - if (eventIt == startIt) + auto events = this->_events[subscriptionId]; + auto eventIt = find_if( + events.begin(), + events.end(), + [this, subscriptionId](Event event) { + return event.id == this->_lastRead[subscriptionId]; + }); + + if (eventIt == events.begin()) { eventIt++; } + this->_lastRead[subscriptionId] = eventIt->id; _events[subscriptionId].erase(_events[subscriptionId].begin()); } }; diff --git a/test/nostr_service_test.cpp b/test/nostr_service_test.cpp index 1679ac5..64c14e8 100644 --- a/test/nostr_service_test.cpp +++ b/test/nostr_service_test.cpp @@ -10,6 +10,7 @@ using std::function; using std::lock_guard; using std::make_shared; +using std::make_unique; using std::mutex; using std::shared_ptr; using std::string; @@ -32,6 +33,15 @@ public: MOCK_METHOD(void, closeConnection, (string uri), (override)); }; +class FakeSigner : public nostr::ISigner +{ +public: + void sign(shared_ptr event) override + { + event->sig = "fake_signature"; + } +}; + class NostrServiceTest : public testing::Test { public: @@ -41,27 +51,45 @@ public: "wss://nostr.thesamecat.io" }; + static const nostr::Event getTestEvent() + { + nostr::Event event; + event.pubkey = "13tn5ccv2guflxgffq4aj0hw5x39pz70zcdrfd6vym887gry38zys28dask"; + event.kind = 1; + event.tags = + { + { "e", "5c83da77af1dec6d7289834998ad7aafbd9e2191396d75ec3cc27f5a77226f36", "wss://nostr.example.com" }, + { "p", "f7234bd4c1394dda46d09f35bd384dd30cc552ad5541990f98844fb06676e9ca" }, + { "a", "30023:f7234bd4c1394dda46d09f35bd384dd30cc552ad5541990f98844fb06676e9ca:abcd", "wss://nostr.example.com" } + }; + event.content = "Hello, World!"; + + return event; + }; + protected: shared_ptr> testAppender; - shared_ptr testClient; + shared_ptr mockClient; + shared_ptr fakeSigner; void SetUp() override { testAppender = make_shared>(); - testClient = make_shared(); + mockClient = make_shared(); + fakeSigner = make_shared(); }; }; TEST_F(NostrServiceTest, Constructor_StartsClient) { - EXPECT_CALL(*testClient, start()).Times(1); + EXPECT_CALL(*mockClient, start()).Times(1); - auto nostrService = new nostr::NostrService(testAppender, testClient); + auto nostrService = make_unique(testAppender, mockClient, fakeSigner); }; TEST_F(NostrServiceTest, Constructor_InitializesService_WithNoDefaultRelays) { - auto nostrService = new nostr::NostrService(testAppender, testClient); + auto nostrService = make_unique(testAppender, mockClient, fakeSigner); auto defaultRelays = nostrService->defaultRelays(); auto activeRelays = nostrService->activeRelays(); @@ -71,7 +99,7 @@ TEST_F(NostrServiceTest, Constructor_InitializesService_WithNoDefaultRelays) TEST_F(NostrServiceTest, Constructor_InitializesService_WithProvidedDefaultRelays) { - auto nostrService = new nostr::NostrService(testAppender, testClient, defaultTestRelays); + auto nostrService = make_unique(testAppender, mockClient, fakeSigner, defaultTestRelays); auto defaultRelays = nostrService->defaultRelays(); auto activeRelays = nostrService->activeRelays(); @@ -85,9 +113,9 @@ TEST_F(NostrServiceTest, Constructor_InitializesService_WithProvidedDefaultRelay TEST_F(NostrServiceTest, Destructor_StopsClient) { - EXPECT_CALL(*testClient, start()).Times(1); + EXPECT_CALL(*mockClient, start()).Times(1); - auto nostrService = new nostr::NostrService(testAppender, testClient); + auto nostrService = make_unique(testAppender, mockClient, fakeSigner); }; TEST_F(NostrServiceTest, OpenRelayConnections_OpensConnections_ToDefaultRelays) @@ -97,10 +125,10 @@ TEST_F(NostrServiceTest, OpenRelayConnections_OpensConnections_ToDefaultRelays) connectionStatus->insert({ defaultTestRelays[0], false }); connectionStatus->insert({ defaultTestRelays[1], false }); - EXPECT_CALL(*testClient, openConnection(defaultTestRelays[0])).Times(1); - EXPECT_CALL(*testClient, openConnection(defaultTestRelays[1])).Times(1); + EXPECT_CALL(*mockClient, openConnection(defaultTestRelays[0])).Times(1); + EXPECT_CALL(*mockClient, openConnection(defaultTestRelays[1])).Times(1); - EXPECT_CALL(*testClient, isConnected(_)) + EXPECT_CALL(*mockClient, isConnected(_)) .WillRepeatedly(Invoke([connectionStatus, &connectionStatusMutex](string uri) { lock_guard lock(connectionStatusMutex); @@ -112,7 +140,7 @@ TEST_F(NostrServiceTest, OpenRelayConnections_OpensConnections_ToDefaultRelays) return status; })); - auto nostrService = new nostr::NostrService(testAppender, testClient, defaultTestRelays); + auto nostrService = make_unique(testAppender, mockClient, fakeSigner, defaultTestRelays); nostrService->openRelayConnections(); auto activeRelays = nostrService->activeRelays(); @@ -131,11 +159,11 @@ TEST_F(NostrServiceTest, OpenRelayConnections_OpensConnections_ToProvidedRelays) auto connectionStatus = make_shared>(); connectionStatus -> insert({ testRelays[0], false }); - EXPECT_CALL(*testClient, openConnection(testRelays[0])).Times(1); - EXPECT_CALL(*testClient, openConnection(defaultTestRelays[0])).Times(0); - EXPECT_CALL(*testClient, openConnection(defaultTestRelays[1])).Times(0); + EXPECT_CALL(*mockClient, openConnection(testRelays[0])).Times(1); + EXPECT_CALL(*mockClient, openConnection(defaultTestRelays[0])).Times(0); + EXPECT_CALL(*mockClient, openConnection(defaultTestRelays[1])).Times(0); - EXPECT_CALL(*testClient, isConnected(_)) + EXPECT_CALL(*mockClient, isConnected(_)) .WillRepeatedly(Invoke([connectionStatus, &connectionStatusMutex](string uri) { lock_guard lock(connectionStatusMutex); @@ -147,7 +175,7 @@ TEST_F(NostrServiceTest, OpenRelayConnections_OpensConnections_ToProvidedRelays) return status; })); - auto nostrService = new nostr::NostrService(testAppender, testClient, defaultTestRelays); + auto nostrService = make_unique(testAppender, mockClient, fakeSigner, defaultTestRelays); nostrService->openRelayConnections(testRelays); auto activeRelays = nostrService->activeRelays(); @@ -168,11 +196,11 @@ TEST_F(NostrServiceTest, OpenRelayConnections_AddsOpenConnections_ToActiveRelays connectionStatus->insert({ defaultTestRelays[1], false }); connectionStatus->insert({ testRelays[0], false }); - EXPECT_CALL(*testClient, openConnection(defaultTestRelays[0])).Times(1); - EXPECT_CALL(*testClient, openConnection(defaultTestRelays[1])).Times(1); - EXPECT_CALL(*testClient, openConnection(testRelays[0])).Times(1); + EXPECT_CALL(*mockClient, openConnection(defaultTestRelays[0])).Times(1); + EXPECT_CALL(*mockClient, openConnection(defaultTestRelays[1])).Times(1); + EXPECT_CALL(*mockClient, openConnection(testRelays[0])).Times(1); - EXPECT_CALL(*testClient, isConnected(_)) + EXPECT_CALL(*mockClient, isConnected(_)) .WillRepeatedly(Invoke([connectionStatus, &connectionStatusMutex](string uri) { lock_guard lock(connectionStatusMutex); @@ -184,7 +212,7 @@ TEST_F(NostrServiceTest, OpenRelayConnections_AddsOpenConnections_ToActiveRelays return status; })); - auto nostrService = new nostr::NostrService(testAppender, testClient, defaultTestRelays); + auto nostrService = make_unique(testAppender, mockClient, fakeSigner, defaultTestRelays); nostrService->openRelayConnections(); auto activeRelays = nostrService->activeRelays(); @@ -215,7 +243,7 @@ TEST_F(NostrServiceTest, CloseRelayConnections_ClosesConnections_ToActiveRelays) connectionStatus->insert({ defaultTestRelays[0], false }); connectionStatus->insert({ defaultTestRelays[1], false }); - EXPECT_CALL(*testClient, isConnected(_)) + EXPECT_CALL(*mockClient, isConnected(_)) .WillRepeatedly(Invoke([connectionStatus, &connectionStatusMutex](string uri) { lock_guard lock(connectionStatusMutex); @@ -227,11 +255,11 @@ TEST_F(NostrServiceTest, CloseRelayConnections_ClosesConnections_ToActiveRelays) return status; })); - auto nostrService = new nostr::NostrService(testAppender, testClient, defaultTestRelays); + auto nostrService = make_unique(testAppender, mockClient, fakeSigner, defaultTestRelays); nostrService->openRelayConnections(); - EXPECT_CALL(*testClient, closeConnection(defaultTestRelays[0])).Times(1); - EXPECT_CALL(*testClient, closeConnection(defaultTestRelays[1])).Times(1); + EXPECT_CALL(*mockClient, closeConnection(defaultTestRelays[0])).Times(1); + EXPECT_CALL(*mockClient, closeConnection(defaultTestRelays[1])).Times(1); nostrService->closeRelayConnections(); @@ -250,7 +278,7 @@ TEST_F(NostrServiceTest, CloseRelayConnections_RemovesClosedConnections_FromActi connectionStatus->insert({ defaultTestRelays[1], false }); connectionStatus->insert({ testRelays[0], false }); - EXPECT_CALL(*testClient, isConnected(_)) + EXPECT_CALL(*mockClient, isConnected(_)) .WillRepeatedly(Invoke([connectionStatus, &connectionStatusMutex](string uri) { lock_guard lock(connectionStatusMutex); @@ -262,10 +290,10 @@ TEST_F(NostrServiceTest, CloseRelayConnections_RemovesClosedConnections_FromActi return status; })); - auto nostrService = new nostr::NostrService(testAppender, testClient, allTestRelays); + auto nostrService = make_unique(testAppender, mockClient, fakeSigner, allTestRelays); nostrService->openRelayConnections(); - EXPECT_CALL(*testClient, closeConnection(testRelays[0])).Times(1); + EXPECT_CALL(*mockClient, closeConnection(testRelays[0])).Times(1); nostrService->closeRelayConnections(testRelays); @@ -288,7 +316,7 @@ TEST_F(NostrServiceTest, PublishEvent_CorrectlyIndicates_AllSuccesses) connectionStatus->insert({ defaultTestRelays[0], false }); connectionStatus->insert({ defaultTestRelays[1], false }); - EXPECT_CALL(*testClient, isConnected(_)) + EXPECT_CALL(*mockClient, isConnected(_)) .WillRepeatedly(Invoke([connectionStatus, &connectionStatusMutex](string uri) { lock_guard lock(connectionStatusMutex); @@ -300,17 +328,18 @@ TEST_F(NostrServiceTest, PublishEvent_CorrectlyIndicates_AllSuccesses) return status; })); - auto nostrService = new nostr::NostrService(testAppender, testClient, defaultTestRelays); + auto nostrService = make_unique(testAppender, mockClient, fakeSigner, defaultTestRelays); nostrService->openRelayConnections(); - EXPECT_CALL(*testClient, send(_, _)) + EXPECT_CALL(*mockClient, send(_, _)) .Times(2) .WillRepeatedly(Invoke([](string message, string uri) { return make_tuple(uri, true); })); - auto [successes, failures] = nostrService->publishEvent(nostr::Event()); + auto testEvent = make_shared(getTestEvent()); + auto [successes, failures] = nostrService->publishEvent(testEvent); ASSERT_EQ(successes.size(), defaultTestRelays.size()); for (auto relay : successes) @@ -328,7 +357,7 @@ TEST_F(NostrServiceTest, PublishEvent_CorrectlyIndicates_AllFailures) connectionStatus->insert({ defaultTestRelays[0], false }); connectionStatus->insert({ defaultTestRelays[1], false }); - EXPECT_CALL(*testClient, isConnected(_)) + EXPECT_CALL(*mockClient, isConnected(_)) .WillRepeatedly(Invoke([connectionStatus, &connectionStatusMutex](string uri) { lock_guard lock(connectionStatusMutex); @@ -340,17 +369,18 @@ TEST_F(NostrServiceTest, PublishEvent_CorrectlyIndicates_AllFailures) return status; })); - auto nostrService = new nostr::NostrService(testAppender, testClient, defaultTestRelays); + auto nostrService = make_unique(testAppender, mockClient, fakeSigner, defaultTestRelays); nostrService->openRelayConnections(); - EXPECT_CALL(*testClient, send(_, _)) + EXPECT_CALL(*mockClient, send(_, _)) .Times(2) .WillRepeatedly(Invoke([](string message, string uri) { return make_tuple(uri, false); })); - auto [successes, failures] = nostrService->publishEvent(nostr::Event()); + auto testEvent = make_shared(getTestEvent()); + auto [successes, failures] = nostrService->publishEvent(testEvent); ASSERT_EQ(successes.size(), 0); @@ -368,7 +398,7 @@ TEST_F(NostrServiceTest, PublishEvent_CorrectlyIndicates_MixedSuccessesAndFailur connectionStatus->insert({ defaultTestRelays[0], false }); connectionStatus->insert({ defaultTestRelays[1], false }); - EXPECT_CALL(*testClient, isConnected(_)) + EXPECT_CALL(*mockClient, isConnected(_)) .WillRepeatedly(Invoke([connectionStatus, &connectionStatusMutex](string uri) { lock_guard lock(connectionStatusMutex); @@ -380,23 +410,24 @@ TEST_F(NostrServiceTest, PublishEvent_CorrectlyIndicates_MixedSuccessesAndFailur return status; })); - auto nostrService = new nostr::NostrService(testAppender, testClient, defaultTestRelays); + auto nostrService = make_unique(testAppender, mockClient, fakeSigner, defaultTestRelays); nostrService->openRelayConnections(); - EXPECT_CALL(*testClient, send(_, defaultTestRelays[0])) + EXPECT_CALL(*mockClient, send(_, defaultTestRelays[0])) .Times(1) .WillRepeatedly(Invoke([](string message, string uri) { return make_tuple(uri, true); })); - EXPECT_CALL(*testClient, send(_, defaultTestRelays[1])) + EXPECT_CALL(*mockClient, send(_, defaultTestRelays[1])) .Times(1) .WillRepeatedly(Invoke([](string message, string uri) { return make_tuple(uri, false); })); - auto [successes, failures] = nostrService->publishEvent(nostr::Event()); + auto testEvent = make_shared(getTestEvent()); + auto [successes, failures] = nostrService->publishEvent(testEvent); ASSERT_EQ(successes.size(), 1); ASSERT_EQ(successes[0], defaultTestRelays[0]); -- cgit From a66a287806ab5a8e9d5a3894287f578c5953de7e Mon Sep 17 00:00:00 2001 From: Michael Jurkoic Date: Mon, 25 Mar 2024 08:07:28 -0500 Subject: Replace Event::deserialize with static methods --- include/nostr.hpp | 10 +++++++++- src/event.cpp | 37 ++++++++++++++++++++++++++++--------- src/nostr_service.cpp | 6 ++---- 3 files changed, 39 insertions(+), 14 deletions(-) (limited to 'src/nostr_service.cpp') diff --git a/include/nostr.hpp b/include/nostr.hpp index 2f37c51..1e462e7 100644 --- a/include/nostr.hpp +++ b/include/nostr.hpp @@ -47,8 +47,16 @@ struct Event /** * @brief Deserializes the event from a JSON string. * @param jsonString A stringified JSON object representing the event. + * @returns An event instance created from the JSON string. */ - void deserialize(std::string jsonString); + static Event fromString(std::string jsonString); + + /** + * @brief Deserializes the event from a JSON object. + * @param j A JSON object representing the event. + * @returns An event instance created from the JSON object. + */ + static Event fromJson(nlohmann::json j); private: /** diff --git a/src/event.cpp b/src/event.cpp index e77e33d..6510ac6 100644 --- a/src/event.cpp +++ b/src/event.cpp @@ -48,16 +48,35 @@ string Event::serialize() return jarr.dump(); }; -void Event::deserialize(string jsonString) +Event Event::fromString(string jstr) { - json j = json::parse(jsonString); - this->id = j["id"]; - this->pubkey = j["pubkey"]; - this->createdAt = j["created_at"]; - this->kind = j["kind"]; - this->tags = j["tags"]; - this->content = j["content"]; - this->sig = j["sig"]; + json j = json::parse(jstr); + Event event; + + event.id = j["id"]; + event.pubkey = j["pubkey"]; + event.createdAt = j["created_at"]; + event.kind = j["kind"]; + event.tags = j["tags"]; + event.content = j["content"]; + event.sig = j["sig"]; + + return event; +}; + +Event Event::fromJson(json j) +{ + Event event; + + event.id = j["id"]; + event.pubkey = j["pubkey"]; + event.createdAt = j["created_at"]; + event.kind = j["kind"]; + event.tags = j["tags"]; + event.content = j["content"]; + event.sig = j["sig"]; + + return event; }; void Event::validate() diff --git a/src/nostr_service.cpp b/src/nostr_service.cpp index 971516f..c7e3158 100644 --- a/src/nostr_service.cpp +++ b/src/nostr_service.cpp @@ -490,10 +490,8 @@ void NostrService::onMessage(string message, function(event)); } // Support other message types here, if necessary. -- cgit From ecc502a5c15a29a9928c8ec462883774bfc9f35a Mon Sep 17 00:00:00 2001 From: Michael Jurkoic Date: Mon, 25 Mar 2024 08:07:38 -0500 Subject: Use shared pointers for filters and events --- include/nostr.hpp | 18 +++++++++++------- src/nostr_service.cpp | 45 +++++++++++++++++++++++++-------------------- 2 files changed, 36 insertions(+), 27 deletions(-) (limited to 'src/nostr_service.cpp') diff --git a/include/nostr.hpp b/include/nostr.hpp index 1e462e7..5f5ce25 100644 --- a/include/nostr.hpp +++ b/include/nostr.hpp @@ -167,7 +167,7 @@ public: * @remarks The service will store a limited number of events returned from the relay for the * given filters. These events may be retrieved via `getNewEvents`. */ - std::string queryRelays(Filters filters); + std::string queryRelays(std::shared_ptr filters); /** * @brief Queries all open relay connections for events matching the given set of filters. @@ -179,21 +179,23 @@ public: * events returned from the relay for the given filters. The service will not store the * events, and they will not be accessible via `getNewEvents`. */ - std::string queryRelays(Filters filters, std::function responseHandler); + std::string queryRelays( + std::shared_ptr filters, + std::function)> responseHandler); /** * @brief Get any new events received since the last call to this method, across all * subscriptions. * @returns A pointer to a vector of new events. */ - std::vector getNewEvents(); + std::vector> getNewEvents(); /** * @brief Get any new events received since the last call to this method, for the given * subscription. * @returns A pointer to a vector of new events. */ - std::vector getNewEvents(std::string subscriptionId); + std::vector> getNewEvents(std::string subscriptionId); /** * @brief Closes the subscription with the given ID on all open relay connections. @@ -237,7 +239,7 @@ private: ///< A map from relay URIs to the subscription IDs open on each relay. std::unordered_map> _subscriptions; ///< A map from subscription IDs to the events returned by the relays for each subscription. - std::unordered_map> _events; + std::unordered_map>> _events; ///< A map from the subscription IDs to the ID of the latest read event for each subscription. std::unordered_map _lastRead; @@ -296,7 +298,9 @@ private: /** * @brief Parses messages received from the relay and invokes the appropriate message handler. */ - void onMessage(std::string message, std::function eventHandler); + void onMessage( + std::string message, + std::function)> eventHandler); /** * @brief A default message handler for events returned from relay queries. @@ -305,7 +309,7 @@ private: * @remark By default, new events are stored in a map of subscription IDs to vectors of events. * Events are retrieved by calling `getNewEvents` or `getNewEvents(subscriptionId)`. */ - void onEvent(std::string subscriptionId, Event event); + void onEvent(std::string subscriptionId, std::shared_ptr event); }; class ISigner diff --git a/src/nostr_service.cpp b/src/nostr_service.cpp index c7e3158..73ce95e 100644 --- a/src/nostr_service.cpp +++ b/src/nostr_service.cpp @@ -20,6 +20,7 @@ using std::find_if; using std::function; using std::future; using std::lock_guard; +using std::make_shared; using std::make_tuple; using std::move; using std::mutex; @@ -172,16 +173,18 @@ tuple NostrService::publishEvent(shared_ptr event) return make_tuple(successfulRelays, failedRelays); }; -string NostrService::queryRelays(Filters filters) +string NostrService::queryRelays(shared_ptr filters) { - return this->queryRelays(filters, [this](string subscriptionId, Event event) { + return this->queryRelays(filters, [this](string subscriptionId, shared_ptr event) { lock_guard lock(this->_propertyMutex); - this->_lastRead[subscriptionId] = event.id; + this->_lastRead[subscriptionId] = event->id; this->onEvent(subscriptionId, event); }); }; -string NostrService::queryRelays(Filters filters, function responseHandler) +string NostrService::queryRelays( + shared_ptr filters, + function)> responseHandler) { RelayList successfulRelays; RelayList failedRelays; @@ -192,7 +195,7 @@ string NostrService::queryRelays(Filters filters, function lock(this->_propertyMutex); this->_subscriptions[relay].push_back(subscriptionId); - string request = filters.serialize(subscriptionId); + string request = filters->serialize(subscriptionId); future> requestFuture = async([this, &relay, &request]() { return this->_client->send(request, relay); @@ -224,20 +227,20 @@ string NostrService::queryRelays(Filters filters, function NostrService::getNewEvents() +vector> NostrService::getNewEvents() { - vector newEvents; + vector> newEvents; for (auto& [subscriptionId, events] : this->_events) { - vector subscriptionEvents = this->getNewEvents(subscriptionId); + vector> subscriptionEvents = this->getNewEvents(subscriptionId); newEvents.insert(newEvents.end(), subscriptionEvents.begin(), subscriptionEvents.end()); } return newEvents; }; -vector NostrService::getNewEvents(string subscriptionId) +vector> NostrService::getNewEvents(string subscriptionId) { if (this->_events.find(subscriptionId) == this->_events.end()) { @@ -252,13 +255,13 @@ vector NostrService::getNewEvents(string subscriptionId) } lock_guard lock(this->_propertyMutex); - vector newEvents; - vector receivedEvents = this->_events[subscriptionId]; - vector::iterator eventIt = find_if( + vector> newEvents; + vector> receivedEvents = this->_events[subscriptionId]; + vector>::iterator eventIt = find_if( receivedEvents.begin(), receivedEvents.end(), - [this,subscriptionId](Event event) { - return event.id == this->_lastRead[subscriptionId]; + [this,subscriptionId](shared_ptr event) { + return event->id == this->_lastRead[subscriptionId]; }) + 1; while (eventIt != receivedEvents.end()) @@ -480,7 +483,9 @@ bool NostrService::hasSubscription(string relay, string subscriptionId) return false; }; -void NostrService::onMessage(string message, function eventHandler) +void NostrService::onMessage( + string message, + function)> eventHandler) { json jarr = json::array(); jarr = json::parse(message); @@ -497,10 +502,10 @@ void NostrService::onMessage(string message, function event) { lock_guard lock(this->_propertyMutex); - this->_events[subscriptionId].push_back(event); + this->_events[subscriptionId].push_back(move(event)); PLOG_INFO << "Received event for subscription: " << subscriptionId; // To protect memory, only keep a limited number of events per subscription. @@ -510,8 +515,8 @@ void NostrService::onEvent(string subscriptionId, Event event) auto eventIt = find_if( events.begin(), events.end(), - [this, subscriptionId](Event event) { - return event.id == this->_lastRead[subscriptionId]; + [this, subscriptionId](shared_ptr event) { + return event->id == this->_lastRead[subscriptionId]; }); if (eventIt == events.begin()) @@ -519,7 +524,7 @@ void NostrService::onEvent(string subscriptionId, Event event) eventIt++; } - this->_lastRead[subscriptionId] = eventIt->id; + this->_lastRead[subscriptionId] = (*eventIt)->id; _events[subscriptionId].erase(_events[subscriptionId].begin()); } }; -- cgit From f694f78597d5b526b359ea3091474c71ef8ad596 Mon Sep 17 00:00:00 2001 From: Michael Jurkoic Date: Sat, 30 Mar 2024 09:44:33 -0500 Subject: Add a unit test for NostrService::QueryRelays --- src/event.cpp | 15 +---- src/filters.cpp | 2 +- src/nostr_service.cpp | 74 +++++++++++++++-------- test/nostr_service_test.cpp | 141 +++++++++++++++++++++++++++++++++++++++++--- 4 files changed, 183 insertions(+), 49 deletions(-) (limited to 'src/nostr_service.cpp') diff --git a/src/event.cpp b/src/event.cpp index 6510ac6..532ba81 100644 --- a/src/event.cpp +++ b/src/event.cpp @@ -43,25 +43,14 @@ string Event::serialize() j["id"] = this->generateId(j.dump()); - json jarr = json::array({ "EVENT", j }); - - return jarr.dump(); + return j.dump(); }; Event Event::fromString(string jstr) { json j = json::parse(jstr); - Event event; - event.id = j["id"]; - event.pubkey = j["pubkey"]; - event.createdAt = j["created_at"]; - event.kind = j["kind"]; - event.tags = j["tags"]; - event.content = j["content"]; - event.sig = j["sig"]; - - return event; + return Event::fromJson(j); }; Event Event::fromJson(json j) diff --git a/src/filters.cpp b/src/filters.cpp index 3179c2f..83756f9 100644 --- a/src/filters.cpp +++ b/src/filters.cpp @@ -44,7 +44,7 @@ string Filters::serialize(string subscriptionId) json jarr = json::array({ "REQ", subscriptionId, j }); - return j.dump(); + return jarr.dump(); }; void Filters::validate() diff --git a/src/nostr_service.cpp b/src/nostr_service.cpp index 73ce95e..d1744e3 100644 --- a/src/nostr_service.cpp +++ b/src/nostr_service.cpp @@ -130,16 +130,21 @@ tuple NostrService::publishEvent(shared_ptr event) PLOG_INFO << "Attempting to publish event to Nostr relays."; - string serializedEvent; + json message; try { this->_signer->sign(event); - serializedEvent = event->serialize(); + message = json::array({ "EVENT", event->serialize() }); } - catch (const std::invalid_argument& error) + catch (const std::invalid_argument& e) { - PLOG_ERROR << "Failed to sign event: " << error.what(); - throw error; + PLOG_ERROR << "Failed to sign event: " << e.what(); + throw e; + } + catch (const json::exception& je) + { + PLOG_ERROR << "Failed to serialize event: " << je.what(); + throw je; } lock_guard lock(this->_propertyMutex); @@ -147,8 +152,9 @@ tuple NostrService::publishEvent(shared_ptr event) for (const string& relay : this->_activeRelays) { PLOG_INFO << "Entering lambda."; - future> publishFuture = async([this, relay, serializedEvent]() { - return this->_client->send(serializedEvent, relay); + future> publishFuture = async([this, relay, message]() + { + return this->_client->send(message.dump(), relay); }); publishFutures.push_back(move(publishFuture)); } @@ -176,7 +182,6 @@ tuple NostrService::publishEvent(shared_ptr event) string NostrService::queryRelays(shared_ptr filters) { return this->queryRelays(filters, [this](string subscriptionId, shared_ptr event) { - lock_guard lock(this->_propertyMutex); this->_lastRead[subscriptionId] = event->id; this->onEvent(subscriptionId, event); }); @@ -190,21 +195,26 @@ string NostrService::queryRelays( RelayList failedRelays; string subscriptionId = this->generateSubscriptionId(); + string request = filters->serialize(subscriptionId); vector>> requestFutures; + vector> receiveFutures; for (const string relay : this->_activeRelays) { - lock_guard lock(this->_propertyMutex); this->_subscriptions[relay].push_back(subscriptionId); - string request = filters->serialize(subscriptionId); - - future> requestFuture = async([this, &relay, &request]() { + + future> requestFuture = async([this, &relay, &request]() + { return this->_client->send(request, relay); }); requestFutures.push_back(move(requestFuture)); - this->_client->receive(relay, [this, responseHandler](string payload) { - this->onMessage(payload, responseHandler); + auto receiveFuture = async([this, &relay, &responseHandler]() + { + this->_client->receive(relay, [this, responseHandler](string payload) { + this->onMessage(payload, responseHandler); + }); }); + receiveFutures.push_back(move(receiveFuture)); } for (auto& publishFuture : requestFutures) @@ -220,6 +230,11 @@ string NostrService::queryRelays( } } + for (auto& receiveFuture : receiveFutures) + { + receiveFuture.get(); + } + size_t targetCount = this->_activeRelays.size(); size_t successfulCount = successfulRelays.size(); PLOG_INFO << "Sent query to " << successfulCount << "/" << targetCount << " open relay connections."; @@ -287,7 +302,8 @@ tuple NostrService::closeSubscription(string subscriptionI } string request = this->generateCloseRequest(subscriptionId); - future> closeFuture = async([this, &relay, &request]() { + future> closeFuture = async([this, &relay, &request]() + { return this->_client->send(request, relay); }); closeFutures.push_back(move(closeFuture)); @@ -326,7 +342,8 @@ tuple NostrService::closeSubscriptions(RelayList relays) vector>> closeFutures; for (const string relay : relays) { - future> closeFuture = async([this, &relay]() { + future> closeFuture = async([this, &relay]() + { RelayList successfulRelays; RelayList failedRelays; @@ -487,19 +504,24 @@ void NostrService::onMessage( string message, function)> eventHandler) { - json jarr = json::array(); - jarr = json::parse(message); - - string messageType = jarr[0]; + try + { + json jMessage = json::parse(message); + string messageType = jMessage[0]; + if (messageType == "EVENT") + { + string subscriptionId = jMessage[1]; + Event event = Event::fromString(jMessage[2]); + eventHandler(subscriptionId, make_shared(event)); + } - if (messageType == "EVENT") + // Support other message types here, if necessary. + } + catch (const json::exception& je) { - string subscriptionId = jarr[1]; - Event event = Event::fromJson(jarr[2]); - eventHandler(subscriptionId, make_shared(event)); + PLOG_ERROR << "JSON handling exception: " << je.what(); + throw je; } - - // Support other message types here, if necessary. }; void NostrService::onEvent(string subscriptionId, shared_ptr event) diff --git a/test/nostr_service_test.cpp b/test/nostr_service_test.cpp index 64c14e8..6b68221 100644 --- a/test/nostr_service_test.cpp +++ b/test/nostr_service_test.cpp @@ -1,12 +1,15 @@ #include #include +#include #include #include +#include #include #include #include +using nlohmann::json; using std::function; using std::lock_guard; using std::make_shared; @@ -67,6 +70,35 @@ public: return event; }; + static const string getTestEventMessage(string subscriptionId) + { + auto event = make_shared(getTestEvent()); + + auto signer = make_unique(); + signer->sign(event); + + json jarr = json::array(); + jarr.push_back("EVENT"); + jarr.push_back(subscriptionId); + jarr.push_back(event->serialize()); + + return jarr.dump(); + } + + static const nostr::Filters getTestFilters() + { + nostr::Filters filters; + filters.authors = { + "13tn5ccv2guflxgffq4aj0hw5x39pz70zcdrfd6vym887gry38zys28dask", + "1l9d9jh67rkwayalrxcy686aujyz5pper5kzjv8jvg8pu9v9ns4ls0xvq42", + "187ujhtmnv82ftg03h4heetwk3dd9mlfkf8th3fvmrk20nxk9mansuzuyla" + }; + filters.kinds = { 0, 1 }; + filters.limit = 10; + + return filters; + } + protected: shared_ptr> testAppender; shared_ptr mockClient; @@ -99,7 +131,11 @@ TEST_F(NostrServiceTest, Constructor_InitializesService_WithNoDefaultRelays) TEST_F(NostrServiceTest, Constructor_InitializesService_WithProvidedDefaultRelays) { - auto nostrService = make_unique(testAppender, mockClient, fakeSigner, defaultTestRelays); + auto nostrService = make_unique( + testAppender, + mockClient, + fakeSigner, + defaultTestRelays); auto defaultRelays = nostrService->defaultRelays(); auto activeRelays = nostrService->activeRelays(); @@ -140,7 +176,11 @@ TEST_F(NostrServiceTest, OpenRelayConnections_OpensConnections_ToDefaultRelays) return status; })); - auto nostrService = make_unique(testAppender, mockClient, fakeSigner, defaultTestRelays); + auto nostrService = make_unique( + testAppender, + mockClient, + fakeSigner, + defaultTestRelays); nostrService->openRelayConnections(); auto activeRelays = nostrService->activeRelays(); @@ -175,7 +215,11 @@ TEST_F(NostrServiceTest, OpenRelayConnections_OpensConnections_ToProvidedRelays) return status; })); - auto nostrService = make_unique(testAppender, mockClient, fakeSigner, defaultTestRelays); + auto nostrService = make_unique( + testAppender, + mockClient, + fakeSigner, + defaultTestRelays); nostrService->openRelayConnections(testRelays); auto activeRelays = nostrService->activeRelays(); @@ -212,7 +256,11 @@ TEST_F(NostrServiceTest, OpenRelayConnections_AddsOpenConnections_ToActiveRelays return status; })); - auto nostrService = make_unique(testAppender, mockClient, fakeSigner, defaultTestRelays); + auto nostrService = make_unique( + testAppender, + mockClient, + fakeSigner, + defaultTestRelays); nostrService->openRelayConnections(); auto activeRelays = nostrService->activeRelays(); @@ -255,7 +303,11 @@ TEST_F(NostrServiceTest, CloseRelayConnections_ClosesConnections_ToActiveRelays) return status; })); - auto nostrService = make_unique(testAppender, mockClient, fakeSigner, defaultTestRelays); + auto nostrService = make_unique( + testAppender, + mockClient, + fakeSigner, + defaultTestRelays); nostrService->openRelayConnections(); EXPECT_CALL(*mockClient, closeConnection(defaultTestRelays[0])).Times(1); @@ -290,7 +342,11 @@ TEST_F(NostrServiceTest, CloseRelayConnections_RemovesClosedConnections_FromActi return status; })); - auto nostrService = make_unique(testAppender, mockClient, fakeSigner, allTestRelays); + auto nostrService = make_unique( + testAppender, + mockClient, + fakeSigner, + allTestRelays); nostrService->openRelayConnections(); EXPECT_CALL(*mockClient, closeConnection(testRelays[0])).Times(1); @@ -328,7 +384,11 @@ TEST_F(NostrServiceTest, PublishEvent_CorrectlyIndicates_AllSuccesses) return status; })); - auto nostrService = make_unique(testAppender, mockClient, fakeSigner, defaultTestRelays); + auto nostrService = make_unique( + testAppender, + mockClient, + fakeSigner, + defaultTestRelays); nostrService->openRelayConnections(); EXPECT_CALL(*mockClient, send(_, _)) @@ -369,7 +429,11 @@ TEST_F(NostrServiceTest, PublishEvent_CorrectlyIndicates_AllFailures) return status; })); - auto nostrService = make_unique(testAppender, mockClient, fakeSigner, defaultTestRelays); + auto nostrService = make_unique( + testAppender, + mockClient, + fakeSigner, + defaultTestRelays); nostrService->openRelayConnections(); EXPECT_CALL(*mockClient, send(_, _)) @@ -410,7 +474,11 @@ TEST_F(NostrServiceTest, PublishEvent_CorrectlyIndicates_MixedSuccessesAndFailur return status; })); - auto nostrService = make_unique(testAppender, mockClient, fakeSigner, defaultTestRelays); + auto nostrService = make_unique( + testAppender, + mockClient, + fakeSigner, + defaultTestRelays); nostrService->openRelayConnections(); EXPECT_CALL(*mockClient, send(_, defaultTestRelays[0])) @@ -435,4 +503,59 @@ TEST_F(NostrServiceTest, PublishEvent_CorrectlyIndicates_MixedSuccessesAndFailur ASSERT_EQ(failures.size(), 1); ASSERT_EQ(failures[0], defaultTestRelays[1]); }; + +TEST_F(NostrServiceTest, QueryRelays_UsesDefaultHandler_AndReturnsSubscriptionId) +{ + mutex connectionStatusMutex; + auto connectionStatus = make_shared>(); + connectionStatus->insert({ defaultTestRelays[0], false }); + connectionStatus->insert({ defaultTestRelays[1], false }); + + EXPECT_CALL(*mockClient, isConnected(_)) + .WillRepeatedly(Invoke([connectionStatus, &connectionStatusMutex](string uri) + { + lock_guard lock(connectionStatusMutex); + bool status = connectionStatus->at(uri); + if (status == false) + { + connectionStatus->at(uri) = true; + } + return status; + })); + + auto nostrService = make_unique( + testAppender, + mockClient, + fakeSigner, + defaultTestRelays); + nostrService->openRelayConnections(); + + auto sentSubscriptionId = make_shared(); + EXPECT_CALL(*mockClient, send(_, _)) + .Times(2) + .WillRepeatedly(Invoke([sentSubscriptionId](string message, string uri) + { + json jarr = json::array(); + jarr = json::parse(message); + + string temp = jarr[1].dump(); + if (!temp.empty() && temp[0] == '\"' && temp[temp.size() - 1] == '\"') + { + *sentSubscriptionId = temp.substr(1, temp.size() - 2); + } + + return make_tuple(uri, true); + })); + EXPECT_CALL(*mockClient, receive(_, _)) + .Times(2) + .WillRepeatedly(Invoke([sentSubscriptionId](string _, function messageHandler) + { + messageHandler(getTestEventMessage(*sentSubscriptionId)); + })); + + auto filters = make_shared(getTestFilters()); + auto receivedSubscriptionId = nostrService->queryRelays(filters); + + EXPECT_STREQ(receivedSubscriptionId.c_str(), sentSubscriptionId->c_str()); +}; } // namespace nostr_test -- cgit From 0a185a13aa4c202ad8d76ac3e62a878dc5f06619 Mon Sep 17 00:00:00 2001 From: Michael Jurkoic Date: Sun, 7 Apr 2024 13:45:33 -0500 Subject: Remove default event handling Caching events and fetching them in batches is out of scope for NostrService. In the future, an additional service should be added to the library that handles local event caching and provides some default handlers for incoming messages from relays. --- include/nostr.hpp | 36 --------------- src/nostr_service.cpp | 81 --------------------------------- test/nostr_service_test.cpp | 106 +++++++++++++++++--------------------------- 3 files changed, 40 insertions(+), 183 deletions(-) (limited to 'src/nostr_service.cpp') diff --git a/include/nostr.hpp b/include/nostr.hpp index 5f5ce25..e450505 100644 --- a/include/nostr.hpp +++ b/include/nostr.hpp @@ -160,15 +160,6 @@ public: */ std::tuple publishEvent(std::shared_ptr event); - /** - * @brief Queries all open relay connections for events matching the given set of filters. - * @param filters The filters to use for the query. - * @returns The ID of the subscription created for the query. - * @remarks The service will store a limited number of events returned from the relay for the - * given filters. These events may be retrieved via `getNewEvents`. - */ - std::string queryRelays(std::shared_ptr filters); - /** * @brief Queries all open relay connections for events matching the given set of filters. * @param filters The filters to use for the query. @@ -182,20 +173,6 @@ public: std::string queryRelays( std::shared_ptr filters, std::function)> responseHandler); - - /** - * @brief Get any new events received since the last call to this method, across all - * subscriptions. - * @returns A pointer to a vector of new events. - */ - std::vector> getNewEvents(); - - /** - * @brief Get any new events received since the last call to this method, for the given - * subscription. - * @returns A pointer to a vector of new events. - */ - std::vector> getNewEvents(std::string subscriptionId); /** * @brief Closes the subscription with the given ID on all open relay connections. @@ -238,10 +215,6 @@ private: RelayList _activeRelays; ///< A map from relay URIs to the subscription IDs open on each relay. std::unordered_map> _subscriptions; - ///< A map from subscription IDs to the events returned by the relays for each subscription. - std::unordered_map>> _events; - ///< A map from the subscription IDs to the ID of the latest read event for each subscription. - std::unordered_map _lastRead; /** * @brief Determines which of the given relays are currently connected. @@ -301,15 +274,6 @@ private: void onMessage( std::string message, std::function)> eventHandler); - - /** - * @brief A default message handler for events returned from relay queries. - * @param subscriptionId The ID of the subscription for which the event was received. - * @param event The event received from the relay. - * @remark By default, new events are stored in a map of subscription IDs to vectors of events. - * Events are retrieved by calling `getNewEvents` or `getNewEvents(subscriptionId)`. - */ - void onEvent(std::string subscriptionId, std::shared_ptr event); }; class ISigner diff --git a/src/nostr_service.cpp b/src/nostr_service.cpp index d1744e3..614e64f 100644 --- a/src/nostr_service.cpp +++ b/src/nostr_service.cpp @@ -179,14 +179,6 @@ tuple NostrService::publishEvent(shared_ptr event) return make_tuple(successfulRelays, failedRelays); }; -string NostrService::queryRelays(shared_ptr filters) -{ - return this->queryRelays(filters, [this](string subscriptionId, shared_ptr event) { - this->_lastRead[subscriptionId] = event->id; - this->onEvent(subscriptionId, event); - }); -}; - string NostrService::queryRelays( shared_ptr filters, function)> responseHandler) @@ -242,52 +234,6 @@ string NostrService::queryRelays( return subscriptionId; }; -vector> NostrService::getNewEvents() -{ - vector> newEvents; - - for (auto& [subscriptionId, events] : this->_events) - { - vector> subscriptionEvents = this->getNewEvents(subscriptionId); - newEvents.insert(newEvents.end(), subscriptionEvents.begin(), subscriptionEvents.end()); - } - - return newEvents; -}; - -vector> NostrService::getNewEvents(string subscriptionId) -{ - if (this->_events.find(subscriptionId) == this->_events.end()) - { - PLOG_ERROR << "No events found for subscription: " << subscriptionId; - throw out_of_range("No events found for subscription: " + subscriptionId); - } - - if (this->_lastRead.find(subscriptionId) == this->_lastRead.end()) - { - PLOG_ERROR << "No last read event ID found for subscription: " << subscriptionId; - throw out_of_range("No last read event ID found for subscription: " + subscriptionId); - } - - lock_guard lock(this->_propertyMutex); - vector> newEvents; - vector> receivedEvents = this->_events[subscriptionId]; - vector>::iterator eventIt = find_if( - receivedEvents.begin(), - receivedEvents.end(), - [this,subscriptionId](shared_ptr event) { - return event->id == this->_lastRead[subscriptionId]; - }) + 1; - - while (eventIt != receivedEvents.end()) - { - newEvents.push_back(move(*eventIt)); - eventIt++; - } - - return newEvents; -}; - tuple NostrService::closeSubscription(string subscriptionId) { RelayList successfulRelays; @@ -523,31 +469,4 @@ void NostrService::onMessage( throw je; } }; - -void NostrService::onEvent(string subscriptionId, shared_ptr event) -{ - lock_guard lock(this->_propertyMutex); - this->_events[subscriptionId].push_back(move(event)); - PLOG_INFO << "Received event for subscription: " << subscriptionId; - - // To protect memory, only keep a limited number of events per subscription. - while (this->_events[subscriptionId].size() > NostrService::MAX_EVENTS_PER_SUBSCRIPTION) - { - auto events = this->_events[subscriptionId]; - auto eventIt = find_if( - events.begin(), - events.end(), - [this, subscriptionId](shared_ptr event) { - return event->id == this->_lastRead[subscriptionId]; - }); - - if (eventIt == events.begin()) - { - eventIt++; - } - - this->_lastRead[subscriptionId] = (*eventIt)->id; - _events[subscriptionId].erase(_events[subscriptionId].begin()); - } -}; } // namespace nostr diff --git a/test/nostr_service_test.cpp b/test/nostr_service_test.cpp index 71af093..7adda7e 100644 --- a/test/nostr_service_test.cpp +++ b/test/nostr_service_test.cpp @@ -54,7 +54,7 @@ public: "wss://nostr.thesamecat.io" }; - static const nostr::Event getTestEvent() + static const nostr::Event getTextNoteTestEvent() { nostr::Event event; event.pubkey = "13tn5ccv2guflxgffq4aj0hw5x39pz70zcdrfd6vym887gry38zys28dask"; @@ -70,10 +70,24 @@ public: return event; }; - static const string getTestEventMessage(string subscriptionId) + static const nostr::Event getLongFormTestEvent() { - auto event = make_shared(getTestEvent()); - + nostr::Event event; + event.pubkey = "13tn5ccv2guflxgffq4aj0hw5x39pz70zcdrfd6vym887gry38zys28dask"; + event.kind = 30023; + event.tags = + { + { "event", "5c83da77af1dec6d7289834998ad7aafbd9e2191396d75ec3cc27f5a77226f36", "wss://nostr.example.com" }, + { "pubkey", "f7234bd4c1394dda46d09f35bd384dd30cc552ad5541990f98844fb06676e9ca" }, + { "author", "30023:f7234bd4c1394dda46d09f35bd384dd30cc552ad5541990f98844fb06676e9ca:abcd", "wss://nostr.example.com" } + }; + event.content = "Hello, World!"; + + return event; + } + + static const string getTestEventMessage(shared_ptr event, string subscriptionId) + { auto signer = make_unique(); signer->sign(event); @@ -85,7 +99,7 @@ public: return jarr.dump(); } - static const nostr::Filters getTestFilters() + static const nostr::Filters getKind0And1TestFilters() { nostr::Filters filters; filters.authors = { @@ -99,6 +113,20 @@ public: return filters; } + static const nostr::Filters getKind30023TestFilters() + { + nostr::Filters filters; + filters.authors = { + "13tn5ccv2guflxgffq4aj0hw5x39pz70zcdrfd6vym887gry38zys28dask", + "1l9d9jh67rkwayalrxcy686aujyz5pper5kzjv8jvg8pu9v9ns4ls0xvq42", + "187ujhtmnv82ftg03h4heetwk3dd9mlfkf8th3fvmrk20nxk9mansuzuyla" + }; + filters.kinds = { 30023 }; + filters.limit = 5; + + return filters; + } + protected: shared_ptr> testAppender; shared_ptr mockClient; @@ -398,7 +426,7 @@ TEST_F(NostrServiceTest, PublishEvent_CorrectlyIndicates_AllSuccesses) return make_tuple(uri, true); })); - auto testEvent = make_shared(getTestEvent()); + auto testEvent = make_shared(getTextNoteTestEvent()); auto [successes, failures] = nostrService->publishEvent(testEvent); ASSERT_EQ(successes.size(), defaultTestRelays.size()); @@ -443,7 +471,7 @@ TEST_F(NostrServiceTest, PublishEvent_CorrectlyIndicates_AllFailures) return make_tuple(uri, false); })); - auto testEvent = make_shared(getTestEvent()); + auto testEvent = make_shared(getTextNoteTestEvent()); auto [successes, failures] = nostrService->publishEvent(testEvent); ASSERT_EQ(successes.size(), 0); @@ -494,7 +522,7 @@ TEST_F(NostrServiceTest, PublishEvent_CorrectlyIndicates_MixedSuccessesAndFailur return make_tuple(uri, false); })); - auto testEvent = make_shared(getTestEvent()); + auto testEvent = make_shared(getTextNoteTestEvent()); auto [successes, failures] = nostrService->publishEvent(testEvent); ASSERT_EQ(successes.size(), 1); @@ -504,61 +532,6 @@ TEST_F(NostrServiceTest, PublishEvent_CorrectlyIndicates_MixedSuccessesAndFailur ASSERT_EQ(failures[0], defaultTestRelays[1]); }; -TEST_F(NostrServiceTest, QueryRelays_UsesDefaultHandler_AndReturnsSubscriptionId) -{ - mutex connectionStatusMutex; - auto connectionStatus = make_shared>(); - connectionStatus->insert({ defaultTestRelays[0], false }); - connectionStatus->insert({ defaultTestRelays[1], false }); - - EXPECT_CALL(*mockClient, isConnected(_)) - .WillRepeatedly(Invoke([connectionStatus, &connectionStatusMutex](string uri) - { - lock_guard lock(connectionStatusMutex); - bool status = connectionStatus->at(uri); - if (status == false) - { - connectionStatus->at(uri) = true; - } - return status; - })); - - auto nostrService = make_unique( - testAppender, - mockClient, - fakeSigner, - defaultTestRelays); - nostrService->openRelayConnections(); - - auto sentSubscriptionId = make_shared(); - EXPECT_CALL(*mockClient, send(_, _)) - .Times(2) - .WillRepeatedly(Invoke([sentSubscriptionId](string message, string uri) - { - json jarr = json::array(); - jarr = json::parse(message); - - string temp = jarr[1].dump(); - if (!temp.empty() && temp[0] == '\"' && temp[temp.size() - 1] == '\"') - { - *sentSubscriptionId = temp.substr(1, temp.size() - 2); - } - - return make_tuple(uri, true); - })); - EXPECT_CALL(*mockClient, receive(_, _)) - .Times(2) - .WillRepeatedly(Invoke([sentSubscriptionId](string _, function messageHandler) - { - messageHandler(getTestEventMessage(*sentSubscriptionId)); - })); - - auto filters = make_shared(getTestFilters()); - auto receivedSubscriptionId = nostrService->queryRelays(filters); - - ASSERT_STREQ(receivedSubscriptionId.c_str(), sentSubscriptionId->c_str()); -}; - TEST_F(NostrServiceTest, QueryRelays_UsesProvidedHandler_AndReturnsSubscriptionId) { mutex connectionStatusMutex; @@ -605,11 +578,12 @@ TEST_F(NostrServiceTest, QueryRelays_UsesProvidedHandler_AndReturnsSubscriptionI .Times(2) .WillRepeatedly(Invoke([sentSubscriptionId](string _, function messageHandler) { - messageHandler(getTestEventMessage(*sentSubscriptionId)); + auto event = make_shared(getTextNoteTestEvent()); + messageHandler(getTestEventMessage(event, *sentSubscriptionId)); })); - auto filters = make_shared(getTestFilters()); - nostr::Event expectedEvent = getTestEvent(); + auto filters = make_shared(getKind0And1TestFilters()); + nostr::Event expectedEvent = getTextNoteTestEvent(); auto receivedSubscriptionId = nostrService->queryRelays( filters, [expectedEvent](const string& subscriptionId, shared_ptr event) -- cgit From c8bb6c8f56e0c6d93c8623722ab932c04de882b5 Mon Sep 17 00:00:00 2001 From: Michael Jurkoic Date: Wed, 10 Apr 2024 21:33:45 -0500 Subject: Handle relay response messages These changes do not yet have unit tests. --- include/client/web_socket_client.hpp | 13 +++ include/nostr.hpp | 44 ++++++++-- src/client/websocketpp_client.cpp | 8 +- src/nostr_service.cpp | 164 +++++++++++++++++++++++++++++------ test/nostr_service_test.cpp | 69 +-------------- 5 files changed, 199 insertions(+), 99 deletions(-) (limited to 'src/nostr_service.cpp') diff --git a/include/client/web_socket_client.hpp b/include/client/web_socket_client.hpp index 3ef2b86..6fbede6 100644 --- a/include/client/web_socket_client.hpp +++ b/include/client/web_socket_client.hpp @@ -42,6 +42,19 @@ public: */ virtual std::tuple send(std::string message, std::string uri) = 0; + /** + * @brief Sends the given message to the given server and sets up a message handler for + * messages received from the server. + * @returns A tuple indicating the server URI and whether the message was successfully + * sent. + * @remark Use this method to send a message and set up a message handler for responses in the + * same call. + */ + virtual std::tuple send( + std::string message, + std::string uri, + std::function messageHandler) = 0; + /** * @brief Sets up a message handler for the given server. * @param uri The URI of the server to which the message handler should be attached. diff --git a/include/nostr.hpp b/include/nostr.hpp index e450505..62eceff 100644 --- a/include/nostr.hpp +++ b/include/nostr.hpp @@ -157,9 +157,23 @@ public: * @returns A tuple of `RelayList` objects, of the form ``, indicating * to which relays the event was published successfully, and to which relays the event failed * to publish. - */ + */ std::tuple publishEvent(std::shared_ptr event); + /** + * @brief Queries all open relay connections for events matching the given set of filters, and + * returns all stored matching events returned by the relays. + * @param filters The filters to use for the query. + * @returns A vector of all events matching the filters from all open relay connections. + * @remark This method runs until the relays send an EOSE message, indicating they have no more + * stored events matching the given filters. When the EOSE message is received, the method + * will close the subscription for each relay and return the received events. + * @remark Use this method to fetch a batch of events from the relays. A `limit` value must be + * set on the filters in the range 1-64, inclusive. If no valid limit is given, it will be + * defaulted to 16. + */ + std::vector> queryRelays(std::shared_ptr filters); + /** * @brief Queries all open relay connections for events matching the given set of filters. * @param filters The filters to use for the query. @@ -172,7 +186,9 @@ public: */ std::string queryRelays( std::shared_ptr filters, - std::function)> responseHandler); + std::function)> eventHandler, + std::function eoseHandler, + std::function closeHandler); /** * @brief Closes the subscription with the given ID on all open relay connections. @@ -269,11 +285,29 @@ private: bool hasSubscription(std::string relay, std::string subscriptionId); /** - * @brief Parses messages received from the relay and invokes the appropriate message handler. + * @brief Parses EVENT messages received from the relay and invokes the given event handler. + * @param message The raw message received from the relay. + * @param eventHandler A callable object that will be invoked with the subscription ID and the + * payload of the event. + * @param eoseHandler A callable object that will be invoked with the subscription ID when the + * relay sends an EOSE message, indicating it has reached the end of stored events for the + * given query. + * @param closeHandler A callable object that will be invoked with the subscription ID and the + * message sent by the relay if the subscription is ended by the relay. */ - void onMessage( + void onSubscriptionMessage( std::string message, - std::function)> eventHandler); + std::function)> eventHandler, + std::function eoseHandler, + std::function closeHandler); + + /** + * @brief Parses OK messages received from the relay and invokes the given acceptance handler. + * @remark The OK message type is sent to indicate whether the relay has accepted an event sent + * by the client. Note that this is distinct from whether the message was successfully sent to + * the relay over the WebSocket connection. + */ + void onAcceptance(std::string message, std::function acceptanceHandler); }; class ISigner diff --git a/src/client/websocketpp_client.cpp b/src/client/websocketpp_client.cpp index 981d4ec..276c5dd 100644 --- a/src/client/websocketpp_client.cpp +++ b/src/client/websocketpp_client.cpp @@ -77,13 +77,19 @@ public: if (error.value() == -1) { - // PLOG_ERROR << "Error publishing event to relay " << relay << ": " << error.message(); return make_tuple(uri, false); } return make_tuple(uri, true); }; + tuple send(string message, string uri, function messageHandler) override + { + auto successes = this->send(message, uri); + this->receive(uri, messageHandler); + return successes; + }; + void receive(string uri, function messageHandler) override { lock_guard lock(this->_propertyMutex); diff --git a/src/nostr_service.cpp b/src/nostr_service.cpp index 614e64f..e8f14f6 100644 --- a/src/nostr_service.cpp +++ b/src/nostr_service.cpp @@ -25,6 +25,7 @@ using std::make_tuple; using std::move; using std::mutex; using std::out_of_range; +using std::promise; using std::shared_ptr; using std::string; using std::thread; @@ -123,6 +124,7 @@ void NostrService::closeRelayConnections(RelayList relays) } }; +// TODO: Make this method return a promise. tuple NostrService::publishEvent(shared_ptr event) { RelayList successfulRelays; @@ -151,12 +153,34 @@ tuple NostrService::publishEvent(shared_ptr event) vector>> publishFutures; for (const string& relay : this->_activeRelays) { - PLOG_INFO << "Entering lambda."; - future> publishFuture = async([this, relay, message]() + promise> publishPromise; + publishFutures.push_back(move(publishPromise.get_future())); + + auto [targetRelay, isSuccess] = this->_client->send( + message.dump(), + relay, + [this, &relay, &event, &publishPromise](string response) + { + this->onAcceptance(response, [this, &relay, &event, &publishPromise](bool isAccepted) + { + if (isAccepted) + { + PLOG_INFO << "Relay " << relay << " accepted event: " << event->id; + publishPromise.set_value(make_tuple(relay, true)); + } + else + { + PLOG_WARNING << "Relay " << relay << " rejected event: " << event->id; + publishPromise.set_value(make_tuple(relay, false)); + } + }); + }); + + if (!isSuccess) { - return this->_client->send(message.dump(), relay); - }); - publishFutures.push_back(move(publishFuture)); + PLOG_WARNING << "Failed to send event to relay: " << relay; + publishPromise.set_value(make_tuple(relay, false)); + } } for (auto& publishFuture : publishFutures) @@ -179,9 +203,72 @@ tuple NostrService::publishEvent(shared_ptr event) return make_tuple(successfulRelays, failedRelays); }; +// TODO: Make this method return a promise. +// TODO: Add a timeout to this method to prevent hanging while waiting for the relay. +vector> NostrService::queryRelays(shared_ptr filters) +{ + if (filters->limit > 64 || filters->limit < 1) + { + PLOG_WARNING << "Filters limit must be between 1 and 64, inclusive. Setting limit to 16."; + filters->limit = 16; + } + + vector> events; + + string subscriptionId = this->generateSubscriptionId(); + string request = filters->serialize(subscriptionId); + vector>> requestFutures; + + // Send the same query to each relay. As events trickle in from each relay, they will be added + // to the events vector. Multiple copies of an event may be received if the same event is + // stored on multiple relays. The function will block until all of the relays send an EOSE or + // CLOSE message. + for (const string relay : this->_activeRelays) + { + promise> eosePromise; + requestFutures.push_back(move(eosePromise.get_future())); + + this->_client->send( + request, + relay, + [this, &relay, &events, &eosePromise](string payload) + { + this->onSubscriptionMessage( + payload, + [&events](const string&, shared_ptr event) + { + events.push_back(event); + }, + [relay, &eosePromise](const string&) + { + eosePromise.set_value(make_tuple(relay, true)); + }, + [relay, &eosePromise](const string&, const string&) + { + eosePromise.set_value(make_tuple(relay, false)); + }); + }); + } + + for (auto& publishFuture : requestFutures) + { + auto [relay, isEose] = publishFuture.get(); + if (!isEose) + { + PLOG_WARNING << "Receive CLOSE message from relay: " << relay; + } + } + + // TODO: De-duplicate events in the vector before returning. + + return events; +}; + string NostrService::queryRelays( shared_ptr filters, - function)> responseHandler) + function)> eventHandler, + function eoseHandler, + function closeHandler) { RelayList successfulRelays; RelayList failedRelays; @@ -189,24 +276,22 @@ string NostrService::queryRelays( string subscriptionId = this->generateSubscriptionId(); string request = filters->serialize(subscriptionId); vector>> requestFutures; - vector> receiveFutures; for (const string relay : this->_activeRelays) { this->_subscriptions[relay].push_back(subscriptionId); - future> requestFuture = async([this, &relay, &request]() - { - return this->_client->send(request, relay); - }); - requestFutures.push_back(move(requestFuture)); - - auto receiveFuture = async([this, &relay, &responseHandler]() - { - this->_client->receive(relay, [this, responseHandler](string payload) { - this->onMessage(payload, responseHandler); + future> requestFuture = async( + [this, &relay, &request, &eventHandler, &eoseHandler, &closeHandler]() + { + return this->_client->send( + request, + relay, + [this, &eventHandler, &eoseHandler, &closeHandler](string payload) + { + this->onSubscriptionMessage(payload, eventHandler, eoseHandler, closeHandler); + }); }); - }); - receiveFutures.push_back(move(receiveFuture)); + requestFutures.push_back(move(requestFuture)); } for (auto& publishFuture : requestFutures) @@ -222,11 +307,6 @@ string NostrService::queryRelays( } } - for (auto& receiveFuture : receiveFutures) - { - receiveFuture.get(); - } - size_t targetCount = this->_activeRelays.size(); size_t successfulCount = successfulRelays.size(); PLOG_INFO << "Sent query to " << successfulCount << "/" << targetCount << " open relay connections."; @@ -446,9 +526,11 @@ bool NostrService::hasSubscription(string relay, string subscriptionId) return false; }; -void NostrService::onMessage( +void NostrService::onSubscriptionMessage( string message, - function)> eventHandler) + function)> eventHandler, + function eoseHandler, + function closeHandler) { try { @@ -460,8 +542,36 @@ void NostrService::onMessage( Event event = Event::fromString(jMessage[2]); eventHandler(subscriptionId, make_shared(event)); } + else if (messageType == "EOSE") + { + string subscriptionId = jMessage[1]; + eoseHandler(subscriptionId); + } + else if (messageType == "CLOSE") + { + string subscriptionId = jMessage[1]; + string reason = jMessage[2]; + closeHandler(subscriptionId, reason); + } + } + catch (const json::exception& je) + { + PLOG_ERROR << "JSON handling exception: " << je.what(); + throw je; + } +}; - // Support other message types here, if necessary. +void NostrService::onAcceptance(string message, function acceptanceHandler) +{ + try + { + json jMessage = json::parse(message); + string messageType = jMessage[0]; + if (messageType == "OK") + { + bool isAccepted = jMessage[2]; + acceptanceHandler(isAccepted); + } } catch (const json::exception& je) { diff --git a/test/nostr_service_test.cpp b/test/nostr_service_test.cpp index 7adda7e..854de78 100644 --- a/test/nostr_service_test.cpp +++ b/test/nostr_service_test.cpp @@ -20,8 +20,10 @@ using std::string; using std::tuple; using std::unordered_map; using ::testing::_; +using ::testing::Args; using ::testing::Invoke; using ::testing::Return; +using ::testing::Truly; namespace nostr_test { @@ -32,6 +34,7 @@ public: MOCK_METHOD(void, openConnection, (string uri), (override)); MOCK_METHOD(bool, isConnected, (string uri), (override)); MOCK_METHOD((tuple), send, (string message, string uri), (override)); + MOCK_METHOD((tuple), send, (string message, string uri, function messageHandler), (override)); MOCK_METHOD(void, receive, (string uri, function messageHandler), (override)); MOCK_METHOD(void, closeConnection, (string uri), (override)); }; @@ -531,70 +534,4 @@ TEST_F(NostrServiceTest, PublishEvent_CorrectlyIndicates_MixedSuccessesAndFailur ASSERT_EQ(failures.size(), 1); ASSERT_EQ(failures[0], defaultTestRelays[1]); }; - -TEST_F(NostrServiceTest, QueryRelays_UsesProvidedHandler_AndReturnsSubscriptionId) -{ - mutex connectionStatusMutex; - auto connectionStatus = make_shared>(); - connectionStatus->insert({ defaultTestRelays[0], false }); - connectionStatus->insert({ defaultTestRelays[1], false }); - - EXPECT_CALL(*mockClient, isConnected(_)) - .WillRepeatedly(Invoke([connectionStatus, &connectionStatusMutex](string uri) - { - lock_guard lock(connectionStatusMutex); - bool status = connectionStatus->at(uri); - if (status == false) - { - connectionStatus->at(uri) = true; - } - return status; - })); - - auto nostrService = make_unique( - testAppender, - mockClient, - fakeSigner, - defaultTestRelays); - nostrService->openRelayConnections(); - - auto sentSubscriptionId = make_shared(); - EXPECT_CALL(*mockClient, send(_, _)) - .Times(2) - .WillRepeatedly(Invoke([sentSubscriptionId](string message, string uri) - { - json jarr = json::array(); - jarr = json::parse(message); - - string temp = jarr[1].dump(); - if (!temp.empty() && temp[0] == '\"' && temp[temp.size() - 1] == '\"') - { - *sentSubscriptionId = temp.substr(1, temp.size() - 2); - } - - return make_tuple(uri, true); - })); - EXPECT_CALL(*mockClient, receive(_, _)) - .Times(2) - .WillRepeatedly(Invoke([sentSubscriptionId](string _, function messageHandler) - { - auto event = make_shared(getTextNoteTestEvent()); - messageHandler(getTestEventMessage(event, *sentSubscriptionId)); - })); - - auto filters = make_shared(getKind0And1TestFilters()); - nostr::Event expectedEvent = getTextNoteTestEvent(); - auto receivedSubscriptionId = nostrService->queryRelays( - filters, - [expectedEvent](const string& subscriptionId, shared_ptr event) - { - ASSERT_STREQ(event->pubkey.c_str(), expectedEvent.pubkey.c_str()); - ASSERT_EQ(event->kind, expectedEvent.kind); - ASSERT_EQ(event->tags.size(), expectedEvent.tags.size()); - ASSERT_STREQ(event->content.c_str(), expectedEvent.content.c_str()); - ASSERT_GT(event->sig.size(), 0); - }); - - ASSERT_STREQ(receivedSubscriptionId.c_str(), sentSubscriptionId->c_str()); -}; } // namespace nostr_test -- cgit From ebbb900849cd5c7ecd471e298c049404c8898b27 Mon Sep 17 00:00:00 2001 From: Michael Jurkoic Date: Thu, 11 Apr 2024 09:26:19 -0500 Subject: Update existing unit tests for recent code changes All preexisting unit tests now pass and test for the correct behavior. --- src/nostr_service.cpp | 5 +++-- test/nostr_service_test.cpp | 37 +++++++++++++++++++++++++++++-------- 2 files changed, 32 insertions(+), 10 deletions(-) (limited to 'src/nostr_service.cpp') diff --git a/src/nostr_service.cpp b/src/nostr_service.cpp index e8f14f6..5b32beb 100644 --- a/src/nostr_service.cpp +++ b/src/nostr_service.cpp @@ -150,8 +150,9 @@ tuple NostrService::publishEvent(shared_ptr event) } lock_guard lock(this->_propertyMutex); + RelayList targetRelays = this->_activeRelays; vector>> publishFutures; - for (const string& relay : this->_activeRelays) + for (const string& relay : targetRelays) { promise> publishPromise; publishFutures.push_back(move(publishPromise.get_future())); @@ -196,7 +197,7 @@ tuple NostrService::publishEvent(shared_ptr event) } } - size_t targetCount = this->_activeRelays.size(); + size_t targetCount = targetRelays.size(); size_t successfulCount = successfulRelays.size(); PLOG_INFO << "Published event to " << successfulCount << "/" << targetCount << " target relays."; diff --git a/test/nostr_service_test.cpp b/test/nostr_service_test.cpp index 854de78..cd6307b 100644 --- a/test/nostr_service_test.cpp +++ b/test/nostr_service_test.cpp @@ -422,10 +422,16 @@ TEST_F(NostrServiceTest, PublishEvent_CorrectlyIndicates_AllSuccesses) defaultTestRelays); nostrService->openRelayConnections(); - EXPECT_CALL(*mockClient, send(_, _)) + EXPECT_CALL(*mockClient, send(_, _, _)) .Times(2) - .WillRepeatedly(Invoke([](string message, string uri) + .WillRepeatedly(Invoke([](string message, string uri, function messageHandler) { + json messageArr = json::parse(message); + auto event = nostr::Event::fromString(messageArr[1]); + + json jarr = json::array({ "OK", event.id, true, "Event accepted" }); + messageHandler(jarr.dump()); + return make_tuple(uri, true); })); @@ -467,9 +473,10 @@ TEST_F(NostrServiceTest, PublishEvent_CorrectlyIndicates_AllFailures) defaultTestRelays); nostrService->openRelayConnections(); - EXPECT_CALL(*mockClient, send(_, _)) + // Simulate a case where the message failed to send to all relays. + EXPECT_CALL(*mockClient, send(_, _, _)) .Times(2) - .WillRepeatedly(Invoke([](string message, string uri) + .WillRepeatedly(Invoke([](string message, string uri, function messageHandler) { return make_tuple(uri, false); })); @@ -512,15 +519,23 @@ TEST_F(NostrServiceTest, PublishEvent_CorrectlyIndicates_MixedSuccessesAndFailur defaultTestRelays); nostrService->openRelayConnections(); - EXPECT_CALL(*mockClient, send(_, defaultTestRelays[0])) + // Simulate a scenario where the message fails to send to one relay, but sends successfully to + // the other, and the relay accepts it. + EXPECT_CALL(*mockClient, send(_, defaultTestRelays[0], _)) .Times(1) - .WillRepeatedly(Invoke([](string message, string uri) + .WillRepeatedly(Invoke([](string message, string uri, function messageHandler) { + json messageArr = json::parse(message); + auto event = nostr::Event::fromString(messageArr[1]); + + json jarr = json::array({ "OK", event.id, true, "Event accepted" }); + messageHandler(jarr.dump()); + return make_tuple(uri, true); })); - EXPECT_CALL(*mockClient, send(_, defaultTestRelays[1])) + EXPECT_CALL(*mockClient, send(_, defaultTestRelays[1], _)) .Times(1) - .WillRepeatedly(Invoke([](string message, string uri) + .WillRepeatedly(Invoke([](string message, string uri, function messageHandler) { return make_tuple(uri, false); })); @@ -534,4 +549,10 @@ TEST_F(NostrServiceTest, PublishEvent_CorrectlyIndicates_MixedSuccessesAndFailur ASSERT_EQ(failures.size(), 1); ASSERT_EQ(failures[0], defaultTestRelays[1]); }; + +// TODO: Add unit tests for events rejected by relays. + +// TODO: Add unit tests for queries. + +// TODO: Add unit tests for closing subscriptions. } // namespace nostr_test -- cgit From cadc670c0d1f61a8e42154837124542749f0c4cd Mon Sep 17 00:00:00 2001 From: Michael Jurkoic Date: Sun, 14 Apr 2024 14:41:25 -0500 Subject: Improve error handling around JSON parsing --- src/event.cpp | 33 +++++++++++++++++++++++++-------- src/nostr_service.cpp | 23 +++++++++++++++++------ 2 files changed, 42 insertions(+), 14 deletions(-) (limited to 'src/nostr_service.cpp') diff --git a/src/event.cpp b/src/event.cpp index 532ba81..7b5bfb2 100644 --- a/src/event.cpp +++ b/src/event.cpp @@ -12,6 +12,7 @@ using nlohmann::json; using std::hex; using std::invalid_argument; using std::make_shared; +using std::ostringstream; using std::setw; using std::setfill; using std::shared_ptr; @@ -49,21 +50,37 @@ string Event::serialize() Event Event::fromString(string jstr) { json j = json::parse(jstr); + Event event; - return Event::fromJson(j); + try + { + event = Event::fromJson(j); + } + catch (const invalid_argument& e) + { + throw e; + } + + return event; }; Event Event::fromJson(json j) { Event event; - event.id = j["id"]; - event.pubkey = j["pubkey"]; - event.createdAt = j["created_at"]; - event.kind = j["kind"]; - event.tags = j["tags"]; - event.content = j["content"]; - event.sig = j["sig"]; + try { + event.id = j.at("id"); + event.pubkey = j.at("pubkey"); + event.createdAt = j.at("created_at"); + event.kind = j.at("kind"); + event.tags = j.at("tags"); + event.content = j.at("content"); + event.sig = j.at("sig"); + } catch (const json::out_of_range& e) { + ostringstream oss; + oss << "Event::fromJson: Tried to access an out-of-range element: " << e.what(); + throw invalid_argument(oss.str()); + } return event; }; diff --git a/src/nostr_service.cpp b/src/nostr_service.cpp index 5b32beb..91e662e 100644 --- a/src/nostr_service.cpp +++ b/src/nostr_service.cpp @@ -19,6 +19,7 @@ using std::async; using std::find_if; using std::function; using std::future; +using std::invalid_argument; using std::lock_guard; using std::make_shared; using std::make_tuple; @@ -536,30 +537,40 @@ void NostrService::onSubscriptionMessage( try { json jMessage = json::parse(message); - string messageType = jMessage[0]; + string messageType = jMessage.at(0); if (messageType == "EVENT") { - string subscriptionId = jMessage[1]; - Event event = Event::fromString(jMessage[2]); + string subscriptionId = jMessage.at(1); + Event event = Event::fromString(jMessage.at(2)); eventHandler(subscriptionId, make_shared(event)); } else if (messageType == "EOSE") { - string subscriptionId = jMessage[1]; + string subscriptionId = jMessage.at(1); eoseHandler(subscriptionId); } else if (messageType == "CLOSE") { - string subscriptionId = jMessage[1]; - string reason = jMessage[2]; + string subscriptionId = jMessage.at(1); + string reason = jMessage.at(2); closeHandler(subscriptionId, reason); } } + catch (const json::out_of_range& joor) + { + PLOG_ERROR << "JSON out-of-range exception: " << joor.what(); + throw joor; + } catch (const json::exception& je) { PLOG_ERROR << "JSON handling exception: " << je.what(); throw je; } + catch (const invalid_argument& ia) + { + PLOG_ERROR << "Invalid argument exception: " << ia.what(); + throw ia; + } }; void NostrService::onAcceptance(string message, function acceptanceHandler) -- cgit From 27f42557a5f6d8a5f1b9a16431edca8129261953 Mon Sep 17 00:00:00 2001 From: Michael Jurkoic Date: Sun, 14 Apr 2024 22:46:13 -0500 Subject: Refine error handling on wss send --- src/nostr_service.cpp | 58 ++++++++++++++++++++++++++++++++------------------- 1 file changed, 37 insertions(+), 21 deletions(-) (limited to 'src/nostr_service.cpp') diff --git a/src/nostr_service.cpp b/src/nostr_service.cpp index 91e662e..8af1e20 100644 --- a/src/nostr_service.cpp +++ b/src/nostr_service.cpp @@ -16,6 +16,7 @@ using boost::uuids::to_string; using boost::uuids::uuid; using nlohmann::json; using std::async; +using std::exception; using std::find_if; using std::function; using std::future; @@ -158,7 +159,7 @@ tuple NostrService::publishEvent(shared_ptr event) promise> publishPromise; publishFutures.push_back(move(publishPromise.get_future())); - auto [targetRelay, isSuccess] = this->_client->send( + auto [uri, success] = this->_client->send( message.dump(), relay, [this, &relay, &event, &publishPromise](string response) @@ -178,7 +179,7 @@ tuple NostrService::publishEvent(shared_ptr event) }); }); - if (!isSuccess) + if (!success) { PLOG_WARNING << "Failed to send event to relay: " << relay; publishPromise.set_value(make_tuple(relay, false)); @@ -230,26 +231,39 @@ vector> NostrService::queryRelays(shared_ptr filters) promise> eosePromise; requestFutures.push_back(move(eosePromise.get_future())); - this->_client->send( - request, - relay, - [this, &relay, &events, &eosePromise](string payload) + try { + auto [uri, success] = this->_client->send( + request, + relay, + [this, &relay, &events, &eosePromise](string payload) + { + this->onSubscriptionMessage( + payload, + [&events](const string&, shared_ptr event) + { + events.push_back(event); + }, + [relay, &eosePromise](const string&) + { + eosePromise.set_value(make_tuple(relay, true)); + }, + [relay, &eosePromise](const string&, const string&) + { + eosePromise.set_value(make_tuple(relay, false)); + }); + }); + + if (!success) { - this->onSubscriptionMessage( - payload, - [&events](const string&, shared_ptr event) - { - events.push_back(event); - }, - [relay, &eosePromise](const string&) - { - eosePromise.set_value(make_tuple(relay, true)); - }, - [relay, &eosePromise](const string&, const string&) - { - eosePromise.set_value(make_tuple(relay, false)); - }); - }); + PLOG_WARNING << "Failed to send query to relay: " << relay; + eosePromise.set_value(make_tuple(uri, false)); + } + } + catch (const exception& e) + { + PLOG_ERROR << "Failed to send query to " << relay << ": " << e.what(); + eosePromise.set_value(make_tuple(relay, false)); + } } for (auto& publishFuture : requestFutures) @@ -282,6 +296,8 @@ string NostrService::queryRelays( { this->_subscriptions[relay].push_back(subscriptionId); + promise> requestPromise; + requestFutures.push_back(move(requestPromise.get_future())); future> requestFuture = async( [this, &relay, &request, &eventHandler, &eoseHandler, &closeHandler]() { -- cgit From c0df21229d4c79bc94dc85a43b798bf0676cb2f1 Mon Sep 17 00:00:00 2001 From: Michael Jurkoic Date: Sun, 14 Apr 2024 23:04:43 -0500 Subject: Take send out of try-catch The client::IWebSocketClient::send method should catch errors and return false if anything goes wrong. --- src/nostr_service.cpp | 53 ++++++++++++++++++++++----------------------------- 1 file changed, 23 insertions(+), 30 deletions(-) (limited to 'src/nostr_service.cpp') diff --git a/src/nostr_service.cpp b/src/nostr_service.cpp index 8af1e20..3dbff62 100644 --- a/src/nostr_service.cpp +++ b/src/nostr_service.cpp @@ -231,38 +231,31 @@ vector> NostrService::queryRelays(shared_ptr filters) promise> eosePromise; requestFutures.push_back(move(eosePromise.get_future())); - try { - auto [uri, success] = this->_client->send( - request, - relay, - [this, &relay, &events, &eosePromise](string payload) - { - this->onSubscriptionMessage( - payload, - [&events](const string&, shared_ptr event) - { - events.push_back(event); - }, - [relay, &eosePromise](const string&) - { - eosePromise.set_value(make_tuple(relay, true)); - }, - [relay, &eosePromise](const string&, const string&) - { - eosePromise.set_value(make_tuple(relay, false)); - }); - }); - - if (!success) + auto [uri, success] = this->_client->send( + request, + relay, + [this, &relay, &events, &eosePromise](string payload) { - PLOG_WARNING << "Failed to send query to relay: " << relay; - eosePromise.set_value(make_tuple(uri, false)); - } - } - catch (const exception& e) + this->onSubscriptionMessage( + payload, + [&events](const string&, shared_ptr event) + { + events.push_back(event); + }, + [relay, &eosePromise](const string&) + { + eosePromise.set_value(make_tuple(relay, true)); + }, + [relay, &eosePromise](const string&, const string&) + { + eosePromise.set_value(make_tuple(relay, false)); + }); + }); + + if (!success) { - PLOG_ERROR << "Failed to send query to " << relay << ": " << e.what(); - eosePromise.set_value(make_tuple(relay, false)); + PLOG_WARNING << "Failed to send query to relay: " << relay; + eosePromise.set_value(make_tuple(uri, false)); } } -- cgit From 4b66a41aff99ef5f7c72f46171dbdf6605c240b4 Mon Sep 17 00:00:00 2001 From: Finrod Felagund Date: Mon, 15 Apr 2024 22:26:34 +0200 Subject: use uuid_v4 to generate faster UUIDs than Boost --- CMakeLists.txt | 18 +++++++++++++----- src/nostr_service.cpp | 19 ++++++++----------- 2 files changed, 21 insertions(+), 16 deletions(-) (limited to 'src/nostr_service.cpp') diff --git a/CMakeLists.txt b/CMakeLists.txt index 4e46b70..0610f3f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,14 +5,22 @@ project(NostrSDK VERSION 0.0.1) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) -set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) -set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) +set (CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}/bin/) +set (CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}/lib/) +set (CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}/lib/) + +if(DEFINED ENV{WORKSPACE}) + list(APPEND CMAKE_PREFIX_PATH $ENV{WORKSPACE}/env/uuid_v4) +else() + list(APPEND CMAKE_PREFIX_PATH ../env/uuid_v4) +endif() # Build the project. set(INCLUDE_DIR ./include) set(CLIENT_INCLUDE_DIR ./include/client) include_directories(${INCLUDE_DIR}) include_directories(${CLIENT_INCLUDE_DIR}) + set(HEADERS ${INCLUDE_DIR}/nostr.hpp ${CLIENT_INCLUDE_DIR}/web_socket_client.hpp @@ -27,21 +35,20 @@ set(SOURCES ${CLIENT_SOURCE_DIR}/websocketpp_client.cpp ) -find_package(Boost REQUIRED COMPONENTS random system) find_package(nlohmann_json CONFIG REQUIRED) +find_package(uuid_v4 REQUIRED) find_package(OpenSSL REQUIRED) find_package(plog CONFIG REQUIRED) find_package(websocketpp CONFIG REQUIRED) add_library(NostrSDK ${SOURCES} ${HEADERS}) target_link_libraries(NostrSDK PRIVATE - Boost::random - Boost::system nlohmann_json::nlohmann_json OpenSSL::SSL OpenSSL::Crypto plog::plog websocketpp::websocketpp + uuid_v4::uuid_v4 ) set_target_properties(NostrSDK PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS YES) @@ -73,6 +80,7 @@ target_link_libraries(NostrSDKTest PRIVATE NostrSDK plog::plog websocketpp::websocketpp + uuid_v4::uuid_v4 ) set_target_properties(NostrSDKTest PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS YES) diff --git a/src/nostr_service.cpp b/src/nostr_service.cpp index 3dbff62..3a59fa6 100644 --- a/src/nostr_service.cpp +++ b/src/nostr_service.cpp @@ -1,19 +1,15 @@ #include -#include -#include -#include + #include #include #include #include #include +#include #include "nostr.hpp" #include "client/web_socket_client.hpp" -using boost::uuids::random_generator; -using boost::uuids::to_string; -using boost::uuids::uuid; using nlohmann::json; using std::async; using std::exception; @@ -49,7 +45,7 @@ NostrService::NostrService( shared_ptr signer, RelayList relays) : _defaultRelays(relays), _client(client), _signer(signer) -{ +{ plog::init(plog::debug, appender.get()); client->start(); }; @@ -178,7 +174,7 @@ tuple NostrService::publishEvent(shared_ptr event) } }); }); - + if (!success) { PLOG_WARNING << "Failed to send event to relay: " << relay; @@ -288,7 +284,7 @@ string NostrService::queryRelays( for (const string relay : this->_activeRelays) { this->_subscriptions[relay].push_back(subscriptionId); - + promise> requestPromise; requestFutures.push_back(move(requestPromise.get_future())); future> requestFuture = async( @@ -517,8 +513,9 @@ void NostrService::disconnect(string relay) string NostrService::generateSubscriptionId() { - uuid uuid = random_generator()(); - return to_string(uuid); + UUIDv4::UUIDGenerator uuidGenerator; + UUIDv4::UUID uuid = uuidGenerator.getUUID(); + return uuid.bytes(); }; string NostrService::generateCloseRequest(string subscriptionId) -- cgit From b0a729a0a79040e0c32142007f4e63ef06d7ae30 Mon Sep 17 00:00:00 2001 From: Finrod Felagund Date: Tue, 16 Apr 2024 16:51:19 +0200 Subject: use namespaces instead of using specific variables --- src/event.cpp | 16 ++-------------- src/filters.cpp | 10 ++-------- src/nostr_service.cpp | 27 +++++---------------------- 3 files changed, 9 insertions(+), 44 deletions(-) (limited to 'src/nostr_service.cpp') diff --git a/src/event.cpp b/src/event.cpp index 2df1c3a..cf6b117 100644 --- a/src/event.cpp +++ b/src/event.cpp @@ -1,24 +1,12 @@ #include #include -#include -#include -#include #include #include #include "nostr.hpp" -using nlohmann::json; -using std::hex; -using std::invalid_argument; -using std::make_shared; -using std::ostringstream; -using std::setw; -using std::setfill; -using std::shared_ptr; -using std::string; -using std::stringstream; -using std::time; +using namespace nlohmann; +using namespace std; namespace nostr { diff --git a/src/filters.cpp b/src/filters.cpp index 83756f9..af9960c 100644 --- a/src/filters.cpp +++ b/src/filters.cpp @@ -1,15 +1,9 @@ -#include -#include -#include #include #include "nostr.hpp" +using namespace nlohmann; +using namespace std; -using nlohmann::json; -using std::invalid_argument; -using std::stringstream; -using std::string; -using std::time; namespace nostr { diff --git a/src/nostr_service.cpp b/src/nostr_service.cpp index 3a59fa6..de40180 100644 --- a/src/nostr_service.cpp +++ b/src/nostr_service.cpp @@ -10,26 +10,9 @@ #include "nostr.hpp" #include "client/web_socket_client.hpp" -using nlohmann::json; -using std::async; -using std::exception; -using std::find_if; -using std::function; -using std::future; -using std::invalid_argument; -using std::lock_guard; -using std::make_shared; -using std::make_tuple; -using std::move; -using std::mutex; -using std::out_of_range; -using std::promise; -using std::shared_ptr; -using std::string; -using std::thread; -using std::tuple; -using std::unique_ptr; -using std::vector; +using namespace nlohmann; +using namespace std; +using namespace UUIDv4; namespace nostr { @@ -513,8 +496,8 @@ void NostrService::disconnect(string relay) string NostrService::generateSubscriptionId() { - UUIDv4::UUIDGenerator uuidGenerator; - UUIDv4::UUID uuid = uuidGenerator.getUUID(); + UUIDGenerator uuidGenerator; + UUID uuid = uuidGenerator.getUUID(); return uuid.bytes(); }; -- cgit From 047a45bd2e3bda3456c1365115d67847d43dd9f1 Mon Sep 17 00:00:00 2001 From: Michael Jurkoic Date: Sun, 28 Apr 2024 09:56:06 -0500 Subject: Configure for Linux builds - Use vcpkg for most dependency management. - Manually include uuid_v4. - Update README with prerequisites and build instructions. - Support subproject and standalone builds. --- .gitignore | 5 ++++- CMakeLists.txt | 56 +++++++++++++++++---------------------------------- CMakePresets.json | 8 ++++---- README.md | 20 +++++++++++++----- include/nostr.hpp | 7 +++++-- src/nostr_service.cpp | 9 --------- 6 files changed, 46 insertions(+), 59 deletions(-) (limited to 'src/nostr_service.cpp') diff --git a/.gitignore b/.gitignore index efb998a..6623964 100644 --- a/.gitignore +++ b/.gitignore @@ -40,8 +40,11 @@ build/ # CMake outputs _deps/ CMakeFiles/ +out/ Makefile CTestTestfile.cmake CMakeCache.txt cmake_install.cmake -OpenSSL-prefix + +# vcpkg +vcpkg_installed/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 208265a..4c521c8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ -cmake_minimum_required(VERSION 3.14) +cmake_minimum_required(VERSION 3.19) cmake_policy(SET CMP0135 NEW) -project(NostrSDK VERSION 0.0.1) +project(NostrSDK VERSION 0.1.0) include(ExternalProject) include(FetchContent) @@ -9,8 +9,6 @@ include(FetchContent) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) -list(APPEND CMAKE_PREFIX_PATH ${CMAKE_SOURCE_DIR}/_deps) - get_directory_property(HAS_PARENT PARENT_DIRECTORY) if(HAS_PARENT) message(STATUS "Configuring as a subproject.") @@ -20,52 +18,36 @@ if(HAS_PARENT) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/../out/${CMAKE_BUILD_TYPE}/lib/) set(CMAKE_INSTALL_PREFIX ${CMAKE_SOURCE_DIR}/../env/) - set(Boost_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/../env/boost/include) + if(DEFINED ENV{WORKSPACE}) + list(APPEND CMAKE_PREFIX_PATH $ENV{WORKSPACE}/env) + else() + list(APPEND CMAKE_PREFIX_PATH ${CMAKE_SOURCE_DIR}/../env) + endif() else() message(STATUS "Configuring as a standalone project.") set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}/bin/) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}/lib/) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}/lib/) - - set(OPENSSL_INCLUDE_DIR ${OPENSSL_ROOT_DIR}/include/) - set(OPENSSL_LIBRARIES ${OPENSSL_ROOT_DIR}/lib/) -endif() - -if(DEFINED ENV{WORKSPACE}) - list(APPEND CMAKE_PREFIX_PATH $ENV{WORKSPACE}/env) -else() - list(APPEND CMAKE_PREFIX_PATH ${CMAKE_SOURCE_DIR}/../env) endif() -#======== Find unmanaged dependencies ========# +#======== Find dependencies ========# +find_package(nlohmann_json CONFIG REQUIRED) find_package(OpenSSL REQUIRED) -find_package(Boost REQUIRED COMPONENTS filesystem system thread regex) +find_package(plog CONFIG REQUIRED) +find_package(websocketpp CONFIG REQUIRED) -#======== Fetch header-only dependencies ========# -FetchContent_Declare( - nlohmann_json - URL https://github.com/nlohmann/json/releases/download/v3.11.3/json.tar.xz - URL_HASH SHA256=d6c65aca6b1ed68e7a182f4757257b107ae403032760ed6ef121c9d55e81757d - USES_TERMINAL_DOWNLOAD TRUE -) -FetchContent_Declare( - plog - URL https://github.com/SergiusTheBest/plog/archive/refs/tags/1.1.10.tar.gz - USES_TERMINAL_DOWNLOAD TRUE -) -FetchContent_Declare( - websocketpp - GIT_REPOSITORY git@github.com:zaphoyd/websocketpp.git - GIT_TAG 0.8.2 -) +#======== Configure uuid_v4 ========# FetchContent_Declare( uuid_v4 - URL https://github.com/crashoz/uuid_v4/archive/refs/tags/v1.0.0.tar.gz - USES_TERMINAL_DOWNLOAD TRUE + GIT_REPOSITORY git@github.com:crashoz/uuid_v4.git + GIT_TAG v1.0.0 ) +FetchContent_Populate(uuid_v4) +set(uuid_v4_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/_deps/uuid_v4-src/) -FetchContent_MakeAvailable(nlohmann_json plog uuid_v4 websocketpp) +find_path(uuid_v4_INCLUDE_DIR uuid_v4.h) +include_directories(${uuid_v4_INCLUDE_DIR}) #======== Build the project ========# set(INCLUDE_DIR ./include) @@ -93,7 +75,6 @@ target_link_libraries(NostrSDK PRIVATE OpenSSL::SSL OpenSSL::Crypto plog::plog - uuid_v4::uuid_v4 websocketpp::websocketpp ) set_target_properties(NostrSDK PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS YES) @@ -124,7 +105,6 @@ target_link_libraries(NostrSDKTest PRIVATE GTest::gtest_main NostrSDK plog::plog - uuid_v4::uuid_v4 websocketpp::websocketpp ) set_target_properties(NostrSDKTest PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS YES) diff --git a/CMakePresets.json b/CMakePresets.json index 8c57178..3e327f5 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -2,12 +2,12 @@ "version": 2, "configurePresets": [ { - "name": "default", - "generator": "Ninja", - "binaryDir": "${sourceDir}/build", + "name": "linux", + "generator": "Unix Makefiles", + "binaryDir": "${sourceDir}/build/linux", "cacheVariables": { "CMAKE_TOOLCHAIN_FILE": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" } } ] - } \ No newline at end of file + } diff --git a/README.md b/README.md index 07ef788..b38e83f 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,21 @@ C++ System Development Kit for Nostr ### Prerequisites -- CMake 3.14 or later +This project uses CMake as its build system, and vcpkg as its dependency manager. Thus, to build the SDK, you will need the following: + +- CMake 3.19 or later - C++17 compiler -- OpenSSL 1.1.1 or later -- Boost 1.85 or later +- vcpkg + +### Build Targets + +The SDK aims to support Linux, Windows, and macOS build targets. CMake presets are provided for each target. + +#### Linux -### Standalone Build +To build the SDK on Linux, run the following commands from the project root: -When building this library as a standalone project, the `Boost_INCLUDE_DIR` and `OPENSSL_ROOT_DIR` variables must be set at the CMake configuration step. +```bash +cmake --preset=linux . +cmake --build ./build/linux +``` diff --git a/include/nostr.hpp b/include/nostr.hpp index d6d5de1..dab4d71 100644 --- a/include/nostr.hpp +++ b/include/nostr.hpp @@ -1,18 +1,21 @@ #pragma once +#include #include #include #include #include +#include #include +#include #include #include - -#include +#include #include #include #include +#include #include "client/web_socket_client.hpp" diff --git a/src/nostr_service.cpp b/src/nostr_service.cpp index de40180..f8565cc 100644 --- a/src/nostr_service.cpp +++ b/src/nostr_service.cpp @@ -1,12 +1,3 @@ -#include - -#include -#include -#include -#include -#include -#include - #include "nostr.hpp" #include "client/web_socket_client.hpp" -- cgit From 1417e31b8d9c181b4c35ff4f50d65125d958689b Mon Sep 17 00:00:00 2001 From: Michael Jurkoic Date: Tue, 30 Apr 2024 00:20:05 -0500 Subject: Ensure first queryRelay unit test passes --- CMakeLists.txt | 1 + include/nostr.hpp | 2 +- src/event.cpp | 3 +-- src/filters.cpp | 8 +++----- src/nostr_service.cpp | 25 ++++++++++++++++++++----- test/nostr_service_test.cpp | 2 ++ 6 files changed, 28 insertions(+), 13 deletions(-) (limited to 'src/nostr_service.cpp') diff --git a/CMakeLists.txt b/CMakeLists.txt index 2ed34df..ce940bb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,6 +8,7 @@ include(FetchContent) # Specify the C++ standard set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -march=native") get_directory_property(HAS_PARENT PARENT_DIRECTORY) if(HAS_PARENT) diff --git a/include/nostr.hpp b/include/nostr.hpp index dab4d71..a59bd33 100644 --- a/include/nostr.hpp +++ b/include/nostr.hpp @@ -114,7 +114,7 @@ struct Filters * @remarks The Nostr client is responsible for managing subscription IDs. Responses from the * relay will be organized by subscription ID. */ - std::string serialize(std::string subscriptionId); + std::string serialize(std::string& subscriptionId); private: /** diff --git a/src/event.cpp b/src/event.cpp index 5c98028..703efae 100644 --- a/src/event.cpp +++ b/src/event.cpp @@ -24,8 +24,7 @@ string Event::serialize() {"kind", this->kind}, {"tags", this->tags}, {"content", this->content}, - {"sig", this->sig} - }; + {"sig", this->sig}}; j["id"] = this->generateId(j.dump()); diff --git a/src/filters.cpp b/src/filters.cpp index cfbc5bf..40596eb 100644 --- a/src/filters.cpp +++ b/src/filters.cpp @@ -3,10 +3,9 @@ using namespace nlohmann; using namespace std; - namespace nostr { -string Filters::serialize(string subscriptionId) +string Filters::serialize(string& subscriptionId) { try { @@ -23,9 +22,8 @@ string Filters::serialize(string subscriptionId) {"kinds", this->kinds}, {"since", this->since}, {"until", this->until}, - {"limit", this->limit} - }; - + {"limit", this->limit}}; + for (auto& tag : this->tags) { stringstream ss; diff --git a/src/nostr_service.cpp b/src/nostr_service.cpp index f8565cc..a1adbbb 100644 --- a/src/nostr_service.cpp +++ b/src/nostr_service.cpp @@ -3,7 +3,6 @@ using namespace nlohmann; using namespace std; -using namespace UUIDv4; namespace nostr { @@ -189,7 +188,23 @@ vector> NostrService::queryRelays(shared_ptr filters) vector> events; string subscriptionId = this->generateSubscriptionId(); - string request = filters->serialize(subscriptionId); + string request; + + try + { + request = filters->serialize(subscriptionId); + } + catch (const invalid_argument& e) + { + PLOG_ERROR << "Failed to serialize filters - invalid object: " << e.what(); + throw e; + } + catch (const json::exception& je) + { + PLOG_ERROR << "Failed to serialize filters - JSON exception: " << je.what(); + throw je; + } + vector>> requestFutures; // Send the same query to each relay. As events trickle in from each relay, they will be added @@ -487,9 +502,9 @@ void NostrService::disconnect(string relay) string NostrService::generateSubscriptionId() { - UUIDGenerator uuidGenerator; - UUID uuid = uuidGenerator.getUUID(); - return uuid.bytes(); + UUIDv4::UUIDGenerator uuidGenerator; + UUIDv4::UUID uuid = uuidGenerator.getUUID(); + return uuid.str(); }; string NostrService::generateCloseRequest(string subscriptionId) diff --git a/test/nostr_service_test.cpp b/test/nostr_service_test.cpp index d23f1bb..e80406e 100644 --- a/test/nostr_service_test.cpp +++ b/test/nostr_service_test.cpp @@ -741,6 +741,7 @@ TEST_F(NostrServiceTest, QueryRelays_ReturnsEvents_UpToEOSE) vector> signedTestEvents; for (nostr::Event testEvent : testEvents) { + std::cout << "TEST: Signing event" << std::endl; auto signedEvent = make_shared(testEvent); signer->sign(signedEvent); @@ -758,6 +759,7 @@ TEST_F(NostrServiceTest, QueryRelays_ReturnsEvents_UpToEOSE) string uri, function messageHandler) { + std::cout << "TEST: Sending message: " << message << std::endl; json messageArr = json::parse(message); string subscriptionId = messageArr.at(1); -- cgit From ae458b29b7c5f9124e6cc4499bed60c865d7badd Mon Sep 17 00:00:00 2001 From: Michael Jurkoic Date: Fri, 3 May 2024 01:15:38 -0500 Subject: Add unit test for queryRelays with callbacks --- include/nostr.hpp | 6 ++- src/nostr_service.cpp | 3 -- test/nostr_service_test.cpp | 105 +++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 108 insertions(+), 6 deletions(-) (limited to 'src/nostr_service.cpp') diff --git a/include/nostr.hpp b/include/nostr.hpp index a59bd33..5e7dbfe 100644 --- a/include/nostr.hpp +++ b/include/nostr.hpp @@ -191,8 +191,12 @@ public: /** * @brief Queries all open relay connections for events matching the given set of filters. * @param filters The filters to use for the query. - * @param responseHandler A callable object that will be invoked each time the client receives + * @param eventHandler A callable object that will be invoked each time the client receives * an event matching the filters. + * @param eoseHandler A callable object that will be invoked when the relay sends an EOSE + * message. + * @param closeHandler A callable object that will be invoked when the relay sends a CLOSE + * message. * @returns The ID of the subscription created for the query. * @remark By providing a response handler, the caller assumes responsibility for handling all * events returned from the relay for the given filters. The service will not store the diff --git a/src/nostr_service.cpp b/src/nostr_service.cpp index a1adbbb..a1f475c 100644 --- a/src/nostr_service.cpp +++ b/src/nostr_service.cpp @@ -273,9 +273,6 @@ string NostrService::queryRelays( for (const string relay : this->_activeRelays) { this->_subscriptions[relay].push_back(subscriptionId); - - promise> requestPromise; - requestFutures.push_back(move(requestPromise.get_future())); future> requestFuture = async( [this, &relay, &request, &eventHandler, &eoseHandler, &closeHandler]() { diff --git a/test/nostr_service_test.cpp b/test/nostr_service_test.cpp index e80406e..b2f6876 100644 --- a/test/nostr_service_test.cpp +++ b/test/nostr_service_test.cpp @@ -16,8 +16,10 @@ using std::lock_guard; using std::make_shared; using std::make_unique; using std::mutex; +using std::promise; using std::shared_ptr; using std::string; +using std::thread; using std::tuple; using std::unordered_map; using std::vector; @@ -741,7 +743,6 @@ TEST_F(NostrServiceTest, QueryRelays_ReturnsEvents_UpToEOSE) vector> signedTestEvents; for (nostr::Event testEvent : testEvents) { - std::cout << "TEST: Signing event" << std::endl; auto signedEvent = make_shared(testEvent); signer->sign(signedEvent); @@ -759,7 +760,6 @@ TEST_F(NostrServiceTest, QueryRelays_ReturnsEvents_UpToEOSE) string uri, function messageHandler) { - std::cout << "TEST: Sending message: " << message << std::endl; json messageArr = json::parse(message); string subscriptionId = messageArr.at(1); @@ -798,5 +798,106 @@ TEST_F(NostrServiceTest, QueryRelays_ReturnsEvents_UpToEOSE) } }; +TEST_F(NostrServiceTest, QueryRelays_CallsHandler_WithReturnedEvents) +{ + mutex connectionStatusMutex; + auto connectionStatus = make_shared>(); + connectionStatus->insert({ defaultTestRelays[0], false }); + connectionStatus->insert({ defaultTestRelays[1], false }); + + EXPECT_CALL(*mockClient, isConnected(_)) + .WillRepeatedly(Invoke([connectionStatus, &connectionStatusMutex](string uri) + { + lock_guard lock(connectionStatusMutex); + bool status = connectionStatus->at(uri); + if (status == false) + { + connectionStatus->at(uri) = true; + } + return status; + })); + + auto signer = make_unique(); + auto nostrService = make_unique( + testAppender, + mockClient, + fakeSigner, + defaultTestRelays); + nostrService->openRelayConnections(); + + auto testEvents = getMultipleTextNoteTestEvents(); + vector> signedTestEvents; + for (nostr::Event testEvent : testEvents) + { + auto signedEvent = make_shared(testEvent); + signer->sign(signedEvent); + + auto serializedEvent = signedEvent->serialize(); + auto deserializedEvent = nostr::Event::fromString(serializedEvent); + + signedEvent = make_shared(deserializedEvent); + signedTestEvents.push_back(signedEvent); + } + + EXPECT_CALL(*mockClient, send(_, _, _)) + .Times(2) + .WillRepeatedly(Invoke([&testEvents, &signer]( + string message, + string uri, + function messageHandler) + { + json messageArr = json::parse(message); + string subscriptionId = messageArr.at(1); + + for (auto event : testEvents) + { + auto sendableEvent = make_shared(event); + signer->sign(sendableEvent); + json jarr = json::array({ "EVENT", subscriptionId, sendableEvent->serialize() }); + messageHandler(jarr.dump()); + } + + json jarr = json::array({ "EOSE", subscriptionId }); + messageHandler(jarr.dump()); + + return make_tuple(uri, true); + })); + + auto filters = make_shared(getKind0And1TestFilters()); + promise eoseReceivedPromise; + auto eoseReceivedFuture = eoseReceivedPromise.get_future(); + int eoseCount = 0; + + string generatedSubscriptionId = nostrService->queryRelays( + filters, + [&generatedSubscriptionId, &signedTestEvents](const string& subscriptionId, shared_ptr event) + { + ASSERT_STREQ(subscriptionId.c_str(), generatedSubscriptionId.c_str()); + ASSERT_NE( + find_if( + signedTestEvents.begin(), + signedTestEvents.end(), + [&event](shared_ptr testEvent) + { + return *testEvent == *event; + }), + signedTestEvents.end()); + }, + [&generatedSubscriptionId, &eoseReceivedPromise, &eoseCount] + (const string& subscriptionId) + { + std::cout << "EOSE received for subscription ID: " << subscriptionId << std::endl; + ASSERT_STREQ(subscriptionId.c_str(), generatedSubscriptionId.c_str()); + + if (++eoseCount == 2) + { + eoseReceivedPromise.set_value(); + } + }, + [](const string&, const string&) {}); + + eoseReceivedFuture.wait(); +}; + // TODO: Add unit tests for closing subscriptions. } // namespace nostr_test -- cgit From 8a170b56b5c53c658af14f82111254e05062a23c Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Sun, 5 May 2024 12:32:42 -0500 Subject: Close relays after batch query and update unit test --- src/nostr_service.cpp | 27 ++++++++++++++++++++++----- test/nostr_service_test.cpp | 10 +++++++--- 2 files changed, 29 insertions(+), 8 deletions(-) (limited to 'src/nostr_service.cpp') diff --git a/src/nostr_service.cpp b/src/nostr_service.cpp index a1f475c..94904ac 100644 --- a/src/nostr_service.cpp +++ b/src/nostr_service.cpp @@ -237,21 +237,35 @@ vector> NostrService::queryRelays(shared_ptr filters) }); }); - if (!success) + if (success) + { + PLOG_INFO << "Sent query to relay: " << relay; + lock_guard lock(this->_propertyMutex); + this->_subscriptions[relay].push_back(subscriptionId); + } + else { PLOG_WARNING << "Failed to send query to relay: " << relay; eosePromise.set_value(make_tuple(uri, false)); } } + // Close open subscriptions and disconnect from relays after events are received. for (auto& publishFuture : requestFutures) { auto [relay, isEose] = publishFuture.get(); - if (!isEose) + if (isEose) + { + PLOG_INFO << "Received EOSE message from relay: " << relay; + } + else { - PLOG_WARNING << "Receive CLOSE message from relay: " << relay; + PLOG_WARNING << "Received CLOSE message from relay: " << relay; + this->closeRelayConnections({ relay }); } } + this->closeSubscription(subscriptionId); + this->closeRelayConnections(this->_activeRelays); // TODO: De-duplicate events in the vector before returning. @@ -272,6 +286,7 @@ string NostrService::queryRelays( vector>> requestFutures; for (const string relay : this->_activeRelays) { + lock_guard lock(this->_propertyMutex); this->_subscriptions[relay].push_back(subscriptionId); future> requestFuture = async( [this, &relay, &request, &eventHandler, &eoseHandler, &closeHandler]() @@ -311,8 +326,8 @@ tuple NostrService::closeSubscription(string subscriptionI { RelayList successfulRelays; RelayList failedRelays; - vector>> closeFutures; + for (const string relay : this->_activeRelays) { if (!this->hasSubscription(relay, subscriptionId)) @@ -321,8 +336,9 @@ tuple NostrService::closeSubscription(string subscriptionI } string request = this->generateCloseRequest(subscriptionId); - future> closeFuture = async([this, &relay, &request]() + future> closeFuture = async([this, relay, request]() { + PLOG_INFO << "Sending " << request << " to relay " << relay; return this->_client->send(request, relay); }); closeFutures.push_back(move(closeFuture)); @@ -512,6 +528,7 @@ string NostrService::generateCloseRequest(string subscriptionId) bool NostrService::hasSubscription(string relay, string subscriptionId) { + lock_guard lock(this->_propertyMutex); auto it = find(this->_subscriptions[relay].begin(), this->_subscriptions[relay].end(), subscriptionId); if (it != this->_subscriptions[relay].end()) // If the subscription is in this->_subscriptions[relay] { diff --git a/test/nostr_service_test.cpp b/test/nostr_service_test.cpp index b2f6876..14eb048 100644 --- a/test/nostr_service_test.cpp +++ b/test/nostr_service_test.cpp @@ -25,6 +25,7 @@ using std::unordered_map; using std::vector; using ::testing::_; using ::testing::Args; +using ::testing::HasSubstr; using ::testing::Invoke; using ::testing::Return; using ::testing::Truly; @@ -711,7 +712,6 @@ TEST_F(NostrServiceTest, PublishEvent_CorrectlyIndicates_EventRejectedBySomeRela ASSERT_EQ(failures[0], defaultTestRelays[1]); }; -// TODO: Add unit tests for queries. TEST_F(NostrServiceTest, QueryRelays_ReturnsEvents_UpToEOSE) { mutex connectionStatusMutex; @@ -753,7 +753,8 @@ TEST_F(NostrServiceTest, QueryRelays_ReturnsEvents_UpToEOSE) signedTestEvents.push_back(signedEvent); } - EXPECT_CALL(*mockClient, send(_, _, _)) + // Expect the query messages. + EXPECT_CALL(*mockClient, send(HasSubstr("REQ"), _, _)) .Times(2) .WillRepeatedly(Invoke([&testEvents, &signer]( string message, @@ -776,6 +777,9 @@ TEST_F(NostrServiceTest, QueryRelays_ReturnsEvents_UpToEOSE) return make_tuple(uri, true); })); + // Expect the close subscription messages after the client receives events. + // TODO: Expect close message. + EXPECT_CALL(*mockClient, send(HasSubstr("CLOSE"), _)).Times(2); auto filters = make_shared(getKind0And1TestFilters()); auto results = nostrService->queryRelays(filters); @@ -886,7 +890,6 @@ TEST_F(NostrServiceTest, QueryRelays_CallsHandler_WithReturnedEvents) [&generatedSubscriptionId, &eoseReceivedPromise, &eoseCount] (const string& subscriptionId) { - std::cout << "EOSE received for subscription ID: " << subscriptionId << std::endl; ASSERT_STREQ(subscriptionId.c_str(), generatedSubscriptionId.c_str()); if (++eoseCount == 2) @@ -900,4 +903,5 @@ TEST_F(NostrServiceTest, QueryRelays_CallsHandler_WithReturnedEvents) }; // TODO: Add unit tests for closing subscriptions. + } // namespace nostr_test -- cgit From 8473ddcdbd6679aeb5ae8cb0cb5a95c3f25d2395 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Mon, 6 May 2024 09:04:26 -0500 Subject: Test closing subscriptions --- include/nostr.hpp | 5 +++-- src/nostr_service.cpp | 22 ++++++++++++++++++++-- test/nostr_service_test.cpp | 44 ++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 63 insertions(+), 8 deletions(-) (limited to 'src/nostr_service.cpp') diff --git a/include/nostr.hpp b/include/nostr.hpp index 5e7dbfe..326a637 100644 --- a/include/nostr.hpp +++ b/include/nostr.hpp @@ -22,7 +22,6 @@ namespace nostr { typedef std::vector RelayList; -typedef std::unordered_map> TagMap; class ISigner; class NostrService; @@ -101,7 +100,7 @@ struct Filters std::vector ids; ///< Event IDs. std::vector authors; ///< Event author npubs. std::vector kinds; ///< Kind numbers. - TagMap tags; ///< Tag names mapped to lists of tag values. + std::unordered_map> tags; ///< Tag names mapped to lists of tag values. std::time_t since; ///< Unix timestamp. Matching events must be newer than this. std::time_t until; ///< Unix timestamp. Matching events must be older than this. int limit; ///< The maximum number of events the relay should return on the initial query. @@ -143,6 +142,8 @@ public: RelayList activeRelays() const; + std::unordered_map> subscriptions() const; + /** * @brief Opens connections to the default Nostr relays of the instance, as specified in * the constructor. diff --git a/src/nostr_service.cpp b/src/nostr_service.cpp index 94904ac..6ffb06d 100644 --- a/src/nostr_service.cpp +++ b/src/nostr_service.cpp @@ -32,6 +32,8 @@ RelayList NostrService::defaultRelays() const { return this->_defaultRelays; }; RelayList NostrService::activeRelays() const { return this->_activeRelays; }; +unordered_map> NostrService::subscriptions() const { return this->_subscriptions; }; + RelayList NostrService::openRelayConnections() { return this->openRelayConnections(this->_defaultRelays); @@ -87,6 +89,9 @@ void NostrService::closeRelayConnections(RelayList relays) this->disconnect(relay); }); disconnectionThreads.push_back(move(disconnectionThread)); + + lock_guard lock(this->_propertyMutex); + this->_subscriptions.erase(relay); } for (thread& disconnectionThread : disconnectionThreads) @@ -286,8 +291,10 @@ string NostrService::queryRelays( vector>> requestFutures; for (const string relay : this->_activeRelays) { - lock_guard lock(this->_propertyMutex); + unique_lock lock(this->_propertyMutex); this->_subscriptions[relay].push_back(subscriptionId); + lock.unlock(); + future> requestFuture = async( [this, &relay, &request, &eventHandler, &eoseHandler, &closeHandler]() { @@ -350,6 +357,13 @@ tuple NostrService::closeSubscription(string subscriptionI if (isSuccess) { successfulRelays.push_back(relay); + + lock_guard lock(this->_propertyMutex); + auto it = find( + this->_subscriptions[relay].begin(), + this->_subscriptions[relay].end(), + subscriptionId); + this->_subscriptions[relay].erase(it); } else { @@ -382,7 +396,11 @@ tuple NostrService::closeSubscriptions(RelayList relays) RelayList successfulRelays; RelayList failedRelays; - for (const string& subscriptionId : this->_subscriptions[relay]) + unique_lock lock(this->_propertyMutex); + auto subscriptionIds = this->_subscriptions[relay]; + lock.unlock(); + + for (const string& subscriptionId : subscriptionIds) { auto [successes, failures] = this->closeSubscription(subscriptionId); successfulRelays.insert(successfulRelays.end(), successes.begin(), successes.end()); diff --git a/test/nostr_service_test.cpp b/test/nostr_service_test.cpp index 14eb048..460be73 100644 --- a/test/nostr_service_test.cpp +++ b/test/nostr_service_test.cpp @@ -778,8 +778,12 @@ TEST_F(NostrServiceTest, QueryRelays_ReturnsEvents_UpToEOSE) return make_tuple(uri, true); })); // Expect the close subscription messages after the client receives events. - // TODO: Expect close message. - EXPECT_CALL(*mockClient, send(HasSubstr("CLOSE"), _)).Times(2); + EXPECT_CALL(*mockClient, send(HasSubstr("CLOSE"), _)) + .Times(2) + .WillRepeatedly(Invoke([](string message, string uri) + { + return make_tuple(uri, true); + })); auto filters = make_shared(getKind0And1TestFilters()); auto results = nostrService->queryRelays(filters); @@ -843,7 +847,7 @@ TEST_F(NostrServiceTest, QueryRelays_CallsHandler_WithReturnedEvents) signedTestEvents.push_back(signedEvent); } - EXPECT_CALL(*mockClient, send(_, _, _)) + EXPECT_CALL(*mockClient, send(HasSubstr("REQ"), _, _)) .Times(2) .WillRepeatedly(Invoke([&testEvents, &signer]( string message, @@ -900,8 +904,40 @@ TEST_F(NostrServiceTest, QueryRelays_CallsHandler_WithReturnedEvents) [](const string&, const string&) {}); eoseReceivedFuture.wait(); + + // Check that the service is keeping track of its active subscriptions. + auto subscriptions = nostrService->subscriptions(); + for (string uri : nostrService->activeRelays()) + { + ASSERT_NO_THROW(subscriptions.at(uri)); + ASSERT_EQ(subscriptions.at(uri).size(), 1); + ASSERT_NE( + find_if( + subscriptions[uri].begin(), + subscriptions[uri].end(), + [&generatedSubscriptionId](const string& subscriptionId) + { + return subscriptionId == generatedSubscriptionId; + }), + subscriptions[uri].end()); + } + + EXPECT_CALL(*mockClient, send(HasSubstr("CLOSE"), _)) + .Times(2) + .WillRepeatedly(Invoke([](string message, string uri) + { + return make_tuple(uri, true); + })); + + nostrService->closeSubscription(generatedSubscriptionId); + + // Check that the service has forgotten about the subscriptions after closing them. + subscriptions = nostrService->subscriptions(); + for (string uri : nostrService->activeRelays()) + { + ASSERT_EQ(subscriptions.at(uri).size(), 0); + } }; // TODO: Add unit tests for closing subscriptions. - } // namespace nostr_test -- cgit From 14ba707c600f13012b3b7f441541f9a6db8ddb8a Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Tue, 7 May 2024 09:07:25 -0500 Subject: Update and test methods for closing subscriptions --- include/nostr.hpp | 36 +++++--- src/nostr_service.cpp | 177 ++++++++++++++++++++++++---------------- test/nostr_service_test.cpp | 195 +++++++++++++++++++++++++++++++++++++++----- 3 files changed, 307 insertions(+), 101 deletions(-) (limited to 'src/nostr_service.cpp') diff --git a/include/nostr.hpp b/include/nostr.hpp index 326a637..e76d1e5 100644 --- a/include/nostr.hpp +++ b/include/nostr.hpp @@ -217,21 +217,25 @@ public: */ std::tuple closeSubscription(std::string subscriptionId); + /** + * @brief Closes the subscription with the given ID on the given relay. + * @returns True if the relay received the CLOSE message, false otherwise. + * @remark If the subscription does not exist on the given relay, or if the relay is not + * connected, the method will do nothing and return false. + */ + bool closeSubscription(std::string subscriptionId, std::string relay); + /** * @brief Closes all open subscriptions on all open relay connections. - * @returns A tuple of `RelayList` objects, of the form ``, indicating - * to which relays the message was sent successfully, and which relays failed to receive the - * message. + * @returns A list of any subscription IDs that failed to close. */ - std::tuple closeSubscriptions(); + std::vector closeSubscriptions(); /** * @brief Closes all open subscriptions on the given relays. - * @returns A tuple of `RelayList` objects, of the form ``, indicating - * to which relays the message was sent successfully, and which relays failed to receive the - * message. + * @returns A list of any subscription IDs that failed to close. */ - std::tuple closeSubscriptions(RelayList relays); + std::vector closeSubscriptions(RelayList relays); private: ///< The maximum number of events the service will store for each subscription. @@ -248,7 +252,7 @@ private: RelayList _defaultRelays; ///< The set of Nostr relays to which the service is currently connected. RelayList _activeRelays; - ///< A map from relay URIs to the subscription IDs open on each relay. + ///< A map from subscription IDs to the relays on which each subscription is open. std::unordered_map> _subscriptions; /** @@ -297,11 +301,17 @@ private: std::string generateCloseRequest(std::string subscriptionId); /** - * @brief Indicates whether the connection to the given relay has a subscription with the given - * ID. - * @returns True if the relay has the subscription, false otherwise. + * @brief Indicates whether the the service has an open subscription with the given ID. + * @returns True if the service has the subscription, false otherwise. + */ + bool hasSubscription(std::string subscriptionId); + + /** + * @brief Indicates whether the service has an open subscription with the given ID on the given + * relay. + * @returns True if the subscription exists on the relay, false otherwise. */ - bool hasSubscription(std::string relay, std::string subscriptionId); + bool hasSubscription(std::string subscriptionId, std::string relay); /** * @brief Parses EVENT messages received from the relay and invokes the given event handler. diff --git a/src/nostr_service.cpp b/src/nostr_service.cpp index 6ffb06d..5443aac 100644 --- a/src/nostr_service.cpp +++ b/src/nostr_service.cpp @@ -90,6 +90,7 @@ void NostrService::closeRelayConnections(RelayList relays) }); disconnectionThreads.push_back(move(disconnectionThread)); + // TODO: Close subscriptions before disconnecting. lock_guard lock(this->_propertyMutex); this->_subscriptions.erase(relay); } @@ -155,7 +156,7 @@ tuple NostrService::publishEvent(shared_ptr event) if (!success) { - PLOG_WARNING << "Failed to send event to relay: " << relay; + PLOG_WARNING << "Failed to send event to relay " << relay; publishPromise.set_value(make_tuple(relay, false)); } } @@ -244,13 +245,13 @@ vector> NostrService::queryRelays(shared_ptr filters) if (success) { - PLOG_INFO << "Sent query to relay: " << relay; + PLOG_INFO << "Sent query to relay " << relay; lock_guard lock(this->_propertyMutex); - this->_subscriptions[relay].push_back(subscriptionId); + this->_subscriptions[subscriptionId].push_back(relay); } else { - PLOG_WARNING << "Failed to send query to relay: " << relay; + PLOG_WARNING << "Failed to send query to relay " << relay; eosePromise.set_value(make_tuple(uri, false)); } } @@ -261,16 +262,15 @@ vector> NostrService::queryRelays(shared_ptr filters) auto [relay, isEose] = publishFuture.get(); if (isEose) { - PLOG_INFO << "Received EOSE message from relay: " << relay; + PLOG_INFO << "Received EOSE message from relay " << relay; } else { - PLOG_WARNING << "Received CLOSE message from relay: " << relay; + PLOG_WARNING << "Received CLOSE message from relay " << relay; this->closeRelayConnections({ relay }); } } this->closeSubscription(subscriptionId); - this->closeRelayConnections(this->_activeRelays); // TODO: De-duplicate events in the vector before returning. @@ -292,7 +292,7 @@ string NostrService::queryRelays( for (const string relay : this->_activeRelays) { unique_lock lock(this->_propertyMutex); - this->_subscriptions[relay].push_back(subscriptionId); + this->_subscriptions[subscriptionId].push_back(relay); lock.unlock(); future> requestFuture = async( @@ -333,97 +333,123 @@ tuple NostrService::closeSubscription(string subscriptionI { RelayList successfulRelays; RelayList failedRelays; + + vector subscriptionRelays; + size_t subscriptionRelayCount; vector>> closeFutures; + + try + { + unique_lock lock(this->_propertyMutex); + subscriptionRelays = this->_subscriptions.at(subscriptionId); + subscriptionRelayCount = subscriptionRelays.size(); + lock.unlock(); + } + catch (const out_of_range& oor) + { + PLOG_WARNING << "Subscription " << subscriptionId << " not found."; + return make_tuple(successfulRelays, failedRelays); + } - for (const string relay : this->_activeRelays) + for (const string relay : subscriptionRelays) { - if (!this->hasSubscription(relay, subscriptionId)) - { - continue; - } - - string request = this->generateCloseRequest(subscriptionId); - future> closeFuture = async([this, relay, request]() + future> closeFuture = async([this, subscriptionId, relay]() { - PLOG_INFO << "Sending " << request << " to relay " << relay; - return this->_client->send(request, relay); + bool success = this->closeSubscription(subscriptionId, relay); + + return make_tuple(relay, success); }); closeFutures.push_back(move(closeFuture)); } for (auto& closeFuture : closeFutures) { - auto [relay, isSuccess] = closeFuture.get(); - if (isSuccess) + auto [uri, success] = closeFuture.get(); + if (success) { - successfulRelays.push_back(relay); - - lock_guard lock(this->_propertyMutex); - auto it = find( - this->_subscriptions[relay].begin(), - this->_subscriptions[relay].end(), - subscriptionId); - this->_subscriptions[relay].erase(it); + successfulRelays.push_back(uri); } else { - failedRelays.push_back(relay); + failedRelays.push_back(uri); } } - size_t targetCount = this->_activeRelays.size(); size_t successfulCount = successfulRelays.size(); - PLOG_INFO << "Sent close request to " << successfulCount << "/" << targetCount << " open relay connections."; + PLOG_INFO << "Sent CLOSE request for subscription " << subscriptionId << " to " << successfulCount << "/" << subscriptionRelayCount << " open relay connections."; + + // If there were no failures, and the subscription has been closed on all of its relays, forget + // about the subscription. + if (failedRelays.empty()) + { + lock_guard lock(this->_propertyMutex); + this->_subscriptions.erase(subscriptionId); + } return make_tuple(successfulRelays, failedRelays); }; -tuple NostrService::closeSubscriptions() +bool NostrService::closeSubscription(string subscriptionId, string relay) { - return this->closeSubscriptions(this->_activeRelays); -}; + if (!this->hasSubscription(subscriptionId, relay)) + { + PLOG_WARNING << "Subscription " << subscriptionId << " not found on relay " << relay; + return false; + } -tuple NostrService::closeSubscriptions(RelayList relays) -{ - RelayList successfulRelays; - RelayList failedRelays; + if (!this->isConnected(relay)) + { + PLOG_WARNING << "Relay " << relay << " is not connected."; + return false; + } + + string request = this->generateCloseRequest(subscriptionId); + auto [uri, success] = this->_client->send(request, relay); - vector>> closeFutures; - for (const string relay : relays) + if (success) { - future> closeFuture = async([this, &relay]() + lock_guard lock(this->_propertyMutex); + auto it = find( + this->_subscriptions[subscriptionId].begin(), + this->_subscriptions[subscriptionId].end(), + relay); + + if (it != this->_subscriptions[subscriptionId].end()) { - RelayList successfulRelays; - RelayList failedRelays; + this->_subscriptions[subscriptionId].erase(it); + } - unique_lock lock(this->_propertyMutex); - auto subscriptionIds = this->_subscriptions[relay]; - lock.unlock(); + PLOG_INFO << "Sent close request for subscription " << subscriptionId << " to relay " << relay; + } + else + { + PLOG_WARNING << "Failed to send close request to relay " << relay; + } - for (const string& subscriptionId : subscriptionIds) - { - auto [successes, failures] = this->closeSubscription(subscriptionId); - successfulRelays.insert(successfulRelays.end(), successes.begin(), successes.end()); - failedRelays.insert(failedRelays.end(), failures.begin(), failures.end()); - } + return success; +}; - return make_tuple(successfulRelays, failedRelays); - }); - closeFutures.push_back(move(closeFuture)); +vector NostrService::closeSubscriptions() +{ + unique_lock lock(this->_propertyMutex); + vector subscriptionIds; + for (auto& [subscriptionId, relays] : this->_subscriptions) + { + subscriptionIds.push_back(subscriptionId); } + lock.unlock(); - for (auto& closeFuture : closeFutures) + vector remainingSubscriptions; + for (const string& subscriptionId : subscriptionIds) { - auto [successes, failures] = closeFuture.get(); - successfulRelays.insert(successfulRelays.end(), successes.begin(), successes.end()); - failedRelays.insert(failedRelays.end(), failures.begin(), failures.end()); + auto [successes, failures] = this->closeSubscription(subscriptionId); + if (!failures.empty()) + { + remainingSubscriptions.push_back(subscriptionId); + } } - size_t targetCount = relays.size(); - size_t successfulCount = successfulRelays.size(); - PLOG_INFO << "Sent close requests to " << successfulCount << "/" << targetCount << " open relay connections."; - - return make_tuple(successfulRelays, failedRelays); + return remainingSubscriptions; }; RelayList NostrService::getConnectedRelays(RelayList relays) @@ -544,15 +570,28 @@ string NostrService::generateCloseRequest(string subscriptionId) return jarr.dump(); }; -bool NostrService::hasSubscription(string relay, string subscriptionId) +bool NostrService::hasSubscription(string subscriptionId) +{ + lock_guard lock(this->_propertyMutex); + auto it = this->_subscriptions.find(subscriptionId); + + return it != this->_subscriptions.end(); +}; + +bool NostrService::hasSubscription(string subscriptionId, string relay) { lock_guard lock(this->_propertyMutex); - auto it = find(this->_subscriptions[relay].begin(), this->_subscriptions[relay].end(), subscriptionId); - if (it != this->_subscriptions[relay].end()) // If the subscription is in this->_subscriptions[relay] + auto subscriptionIt = this->_subscriptions.find(subscriptionId); + + if (subscriptionIt == this->_subscriptions.end()) { - return true; + return false; } - return false; + + auto relays = this->_subscriptions[subscriptionId]; + auto relayIt = find(relays.begin(), relays.end(), relay); + + return relayIt != relays.end(); }; void NostrService::onSubscriptionMessage( diff --git a/test/nostr_service_test.cpp b/test/nostr_service_test.cpp index 460be73..0f0d439 100644 --- a/test/nostr_service_test.cpp +++ b/test/nostr_service_test.cpp @@ -804,6 +804,9 @@ TEST_F(NostrServiceTest, QueryRelays_ReturnsEvents_UpToEOSE) }), signedTestEvents.end()); } + + auto subscriptions = nostrService->subscriptions(); + ASSERT_TRUE(subscriptions.empty()); }; TEST_F(NostrServiceTest, QueryRelays_CallsHandler_WithReturnedEvents) @@ -907,20 +910,8 @@ TEST_F(NostrServiceTest, QueryRelays_CallsHandler_WithReturnedEvents) // Check that the service is keeping track of its active subscriptions. auto subscriptions = nostrService->subscriptions(); - for (string uri : nostrService->activeRelays()) - { - ASSERT_NO_THROW(subscriptions.at(uri)); - ASSERT_EQ(subscriptions.at(uri).size(), 1); - ASSERT_NE( - find_if( - subscriptions[uri].begin(), - subscriptions[uri].end(), - [&generatedSubscriptionId](const string& subscriptionId) - { - return subscriptionId == generatedSubscriptionId; - }), - subscriptions[uri].end()); - } + ASSERT_NO_THROW(subscriptions.at(generatedSubscriptionId)); + ASSERT_EQ(subscriptions.at(generatedSubscriptionId).size(), 2); EXPECT_CALL(*mockClient, send(HasSubstr("CLOSE"), _)) .Times(2) @@ -929,15 +920,181 @@ TEST_F(NostrServiceTest, QueryRelays_CallsHandler_WithReturnedEvents) return make_tuple(uri, true); })); - nostrService->closeSubscription(generatedSubscriptionId); + auto [successes, failures] = nostrService->closeSubscription(generatedSubscriptionId); + + ASSERT_TRUE(failures.empty()); // Check that the service has forgotten about the subscriptions after closing them. subscriptions = nostrService->subscriptions(); - for (string uri : nostrService->activeRelays()) + ASSERT_TRUE(subscriptions.empty()); +}; + +TEST_F(NostrServiceTest, Service_MaintainsMultipleSubscriptions_ThenClosesAll) +{ + // Mock connections. + mutex connectionStatusMutex; + auto connectionStatus = make_shared>(); + vector testRelays = { "wss://theforest.nostr1.com" }; + connectionStatus->insert({ testRelays[0], false }); + + EXPECT_CALL(*mockClient, isConnected(_)) + .WillRepeatedly(Invoke([connectionStatus, &connectionStatusMutex](string uri) + { + lock_guard lock(connectionStatusMutex); + bool status = connectionStatus->at(uri); + if (status == false) + { + connectionStatus->at(uri) = true; + } + return status; + })); + + auto signer = make_unique(); + auto nostrService = make_unique( + testAppender, + mockClient, + fakeSigner, + testRelays); + nostrService->openRelayConnections(); + + // Mock relay responses. + auto testEvents = getMultipleTextNoteTestEvents(); + vector> signedTestEvents; + for (nostr::Event testEvent : testEvents) { - ASSERT_EQ(subscriptions.at(uri).size(), 0); + auto signedEvent = make_shared(testEvent); + signer->sign(signedEvent); + + auto serializedEvent = signedEvent->serialize(); + auto deserializedEvent = nostr::Event::fromString(serializedEvent); + + signedEvent = make_shared(deserializedEvent); + signedTestEvents.push_back(signedEvent); } -}; -// TODO: Add unit tests for closing subscriptions. + vector subscriptionIds; + EXPECT_CALL(*mockClient, send(HasSubstr("REQ"), _, _)) + .Times(2) + .WillOnce(Invoke([&testEvents, &signer, &subscriptionIds]( + string message, + string uri, + function messageHandler) + { + json messageArr = json::parse(message); + subscriptionIds.push_back(messageArr.at(1)); + + for (auto event : testEvents) + { + auto sendableEvent = make_shared(event); + signer->sign(sendableEvent); + json jarr = json::array({ "EVENT", subscriptionIds.at(0), sendableEvent->serialize() }); + messageHandler(jarr.dump()); + } + + json jarr = json::array({ "EOSE", subscriptionIds.at(0), }); + messageHandler(jarr.dump()); + + return make_tuple(uri, true); + })) + .WillOnce(Invoke([&testEvents, &signer, &subscriptionIds]( + string message, + string uri, + function messageHandler) + { + json messageArr = json::parse(message); + subscriptionIds.push_back(messageArr.at(1)); + + for (auto event : testEvents) + { + auto sendableEvent = make_shared(event); + signer->sign(sendableEvent); + json jarr = json::array({ "EVENT", subscriptionIds.at(1), sendableEvent->serialize() }); + messageHandler(jarr.dump()); + } + + json jarr = json::array({ "EOSE", subscriptionIds.at(1), }); + messageHandler(jarr.dump()); + + return make_tuple(uri, true); + })); + + // Send queries. + auto shortFormFilters = make_shared(getKind0And1TestFilters()); + auto longFormFilters = make_shared(getKind30023TestFilters()); + promise shortFormPromise; + promise longFormPromise; + auto shortFormFuture = shortFormPromise.get_future(); + auto longFormFuture = longFormPromise.get_future(); + + string shortFormSubscriptionId = nostrService->queryRelays( + shortFormFilters, + [&shortFormSubscriptionId, &signedTestEvents](const string& subscriptionId, shared_ptr event) + { + ASSERT_STREQ(subscriptionId.c_str(), shortFormSubscriptionId.c_str()); + ASSERT_NE( + find_if( + signedTestEvents.begin(), + signedTestEvents.end(), + [&event](shared_ptr testEvent) + { + return *testEvent == *event; + }), + signedTestEvents.end()); + }, + [&shortFormSubscriptionId, &shortFormPromise] + (const string& subscriptionId) + { + ASSERT_STREQ(subscriptionId.c_str(), shortFormSubscriptionId.c_str()); + shortFormPromise.set_value(); + }, + [](const string&, const string&) {}); + string longFormSubscriptionId = nostrService->queryRelays( + shortFormFilters, + [&longFormSubscriptionId, &signedTestEvents](const string& subscriptionId, shared_ptr event) + { + ASSERT_STREQ(subscriptionId.c_str(), longFormSubscriptionId.c_str()); + ASSERT_NE( + find_if( + signedTestEvents.begin(), + signedTestEvents.end(), + [&event](shared_ptr testEvent) + { + return *testEvent == *event; + }), + signedTestEvents.end()); + }, + [&longFormSubscriptionId, &longFormPromise] + (const string& subscriptionId) + { + ASSERT_STREQ(subscriptionId.c_str(), longFormSubscriptionId.c_str()); + longFormPromise.set_value(); + }, + [](const string&, const string&) {}); + + shortFormFuture.wait(); + longFormFuture.wait(); + + // Check that the service has opened a subscription for each query. + auto subscriptions = nostrService->subscriptions(); + ASSERT_NO_THROW(subscriptions.at(shortFormSubscriptionId)); + ASSERT_EQ(subscriptions.at(shortFormSubscriptionId).size(), 1); + ASSERT_NO_THROW(subscriptions.at(longFormSubscriptionId)); + ASSERT_EQ(subscriptions.at(longFormSubscriptionId).size(), 1); + + // Mock the relay response for closing subscriptions. + EXPECT_CALL(*mockClient, send(HasSubstr("CLOSE"), _)) + .Times(2) + .WillRepeatedly(Invoke([](string message, string uri) + { + return make_tuple(uri, true); + })); + + // Close all subscriptions maintained by the service. + auto remainingSubscriptions = nostrService->closeSubscriptions(); + ASSERT_TRUE(remainingSubscriptions.empty()); + + // Check that all subscriptions have been closed. + subscriptions = nostrService->subscriptions(); + ASSERT_TRUE(subscriptions.empty()); +}; } // namespace nostr_test -- cgit From d6faf6c815611450d1b61045b53525d7f25ac5c9 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Tue, 7 May 2024 09:22:21 -0500 Subject: Remove 'RelayList' type alias --- include/nostr.hpp | 32 +++++++++++++++----------------- src/nostr_service.cpp | 42 +++++++++++++++++++++--------------------- test/nostr_service_test.cpp | 31 ++++++++----------------------- 3 files changed, 44 insertions(+), 61 deletions(-) (limited to 'src/nostr_service.cpp') diff --git a/include/nostr.hpp b/include/nostr.hpp index e76d1e5..e5b29c7 100644 --- a/include/nostr.hpp +++ b/include/nostr.hpp @@ -21,8 +21,6 @@ namespace nostr { -typedef std::vector RelayList; - class ISigner; class NostrService; @@ -135,12 +133,12 @@ public: std::shared_ptr appender, std::shared_ptr client, std::shared_ptr signer, - RelayList relays); + std::vector relays); ~NostrService(); - RelayList defaultRelays() const; + std::vector defaultRelays() const; - RelayList activeRelays() const; + std::vector activeRelays() const; std::unordered_map> subscriptions() const; @@ -149,13 +147,13 @@ public: * the constructor. * @return A list of the relay URLs to which connections were successfully opened. */ - RelayList openRelayConnections(); + std::vector openRelayConnections(); /** * @brief Opens connections to the specified Nostr relays. * @returns A list of the relay URLs to which connections were successfully opened. */ - RelayList openRelayConnections(RelayList relays); + std::vector openRelayConnections(std::vector relays); /** * @brief Closes all open relay connections. @@ -165,15 +163,15 @@ public: /** * @brief Closes any open connections to the specified Nostr relays. */ - void closeRelayConnections(RelayList relays); + void closeRelayConnections(std::vector relays); /** * @brief Publishes a Nostr event to all open relay connections. - * @returns A tuple of `RelayList` objects, of the form ``, indicating + * @returns A tuple of `std::vector` objects, of the form ``, indicating * to which relays the event was published successfully, and to which relays the event failed * to publish. */ - std::tuple publishEvent(std::shared_ptr event); + std::tuple, std::vector> publishEvent(std::shared_ptr event); /** * @brief Queries all open relay connections for events matching the given set of filters, and @@ -211,11 +209,11 @@ public: /** * @brief Closes the subscription with the given ID on all open relay connections. - * @returns A tuple of `RelayList` objects, of the form ``, indicating + * @returns A tuple of `std::vector` objects, of the form ``, indicating * to which relays the message was sent successfully, and which relays failed to receive the * message. */ - std::tuple closeSubscription(std::string subscriptionId); + std::tuple, std::vector> closeSubscription(std::string subscriptionId); /** * @brief Closes the subscription with the given ID on the given relay. @@ -235,7 +233,7 @@ public: * @brief Closes all open subscriptions on the given relays. * @returns A list of any subscription IDs that failed to close. */ - std::vector closeSubscriptions(RelayList relays); + std::vector closeSubscriptions(std::vector relays); private: ///< The maximum number of events the service will store for each subscription. @@ -249,9 +247,9 @@ private: ///< A mutex to protect the instance properties. std::mutex _propertyMutex; ///< The default set of Nostr relays to which the service will attempt to connect. - RelayList _defaultRelays; + std::vector _defaultRelays; ///< The set of Nostr relays to which the service is currently connected. - RelayList _activeRelays; + std::vector _activeRelays; ///< A map from subscription IDs to the relays on which each subscription is open. std::unordered_map> _subscriptions; @@ -259,13 +257,13 @@ private: * @brief Determines which of the given relays are currently connected. * @returns A list of the URIs of currently-open relay connections from the given list. */ - RelayList getConnectedRelays(RelayList relays); + std::vector getConnectedRelays(std::vector relays); /** * @brief Determines which of the given relays are not currently connected. * @returns A list of the URIs of currently-unconnected relays from the given list. */ - RelayList getUnconnectedRelays(RelayList relays); + std::vector getUnconnectedRelays(std::vector relays); /** * @brief Determines whether the given relay is currently connected. diff --git a/src/nostr_service.cpp b/src/nostr_service.cpp index 5443aac..664243f 100644 --- a/src/nostr_service.cpp +++ b/src/nostr_service.cpp @@ -16,7 +16,7 @@ NostrService::NostrService( shared_ptr appender, shared_ptr client, shared_ptr signer, - RelayList relays) + vector relays) : _defaultRelays(relays), _client(client), _signer(signer) { plog::init(plog::debug, appender.get()); @@ -28,21 +28,21 @@ NostrService::~NostrService() this->_client->stop(); }; -RelayList NostrService::defaultRelays() const { return this->_defaultRelays; }; +vector NostrService::defaultRelays() const { return this->_defaultRelays; }; -RelayList NostrService::activeRelays() const { return this->_activeRelays; }; +vector NostrService::activeRelays() const { return this->_activeRelays; }; unordered_map> NostrService::subscriptions() const { return this->_subscriptions; }; -RelayList NostrService::openRelayConnections() +vector NostrService::openRelayConnections() { return this->openRelayConnections(this->_defaultRelays); }; -RelayList NostrService::openRelayConnections(RelayList relays) +vector NostrService::openRelayConnections(vector relays) { PLOG_INFO << "Attempting to connect to Nostr relays."; - RelayList unconnectedRelays = this->getUnconnectedRelays(relays); + vector unconnectedRelays = this->getUnconnectedRelays(relays); vector connectionThreads; for (string relay : unconnectedRelays) @@ -77,10 +77,10 @@ void NostrService::closeRelayConnections() this->closeRelayConnections(this->_activeRelays); }; -void NostrService::closeRelayConnections(RelayList relays) +void NostrService::closeRelayConnections(vector relays) { PLOG_INFO << "Disconnecting from Nostr relays."; - RelayList connectedRelays = getConnectedRelays(relays); + vector connectedRelays = getConnectedRelays(relays); vector disconnectionThreads; for (string relay : connectedRelays) @@ -102,10 +102,10 @@ void NostrService::closeRelayConnections(RelayList relays) }; // TODO: Make this method return a promise. -tuple NostrService::publishEvent(shared_ptr event) +tuple, vector> NostrService::publishEvent(shared_ptr event) { - RelayList successfulRelays; - RelayList failedRelays; + vector successfulRelays; + vector failedRelays; PLOG_INFO << "Attempting to publish event to Nostr relays."; @@ -127,7 +127,7 @@ tuple NostrService::publishEvent(shared_ptr event) } lock_guard lock(this->_propertyMutex); - RelayList targetRelays = this->_activeRelays; + vector targetRelays = this->_activeRelays; vector>> publishFutures; for (const string& relay : targetRelays) { @@ -283,8 +283,8 @@ string NostrService::queryRelays( function eoseHandler, function closeHandler) { - RelayList successfulRelays; - RelayList failedRelays; + vector successfulRelays; + vector failedRelays; string subscriptionId = this->generateSubscriptionId(); string request = filters->serialize(subscriptionId); @@ -329,10 +329,10 @@ string NostrService::queryRelays( return subscriptionId; }; -tuple NostrService::closeSubscription(string subscriptionId) +tuple, vector> NostrService::closeSubscription(string subscriptionId) { - RelayList successfulRelays; - RelayList failedRelays; + vector successfulRelays; + vector failedRelays; vector subscriptionRelays; size_t subscriptionRelayCount; @@ -452,10 +452,10 @@ vector NostrService::closeSubscriptions() return remainingSubscriptions; }; -RelayList NostrService::getConnectedRelays(RelayList relays) +vector NostrService::getConnectedRelays(vector relays) { PLOG_VERBOSE << "Identifying connected relays."; - RelayList connectedRelays; + vector connectedRelays; for (string relay : relays) { bool isActive = find(this->_activeRelays.begin(), this->_activeRelays.end(), relay) @@ -480,10 +480,10 @@ RelayList NostrService::getConnectedRelays(RelayList relays) return connectedRelays; }; -RelayList NostrService::getUnconnectedRelays(RelayList relays) +vector NostrService::getUnconnectedRelays(vector relays) { PLOG_VERBOSE << "Identifying unconnected relays."; - RelayList unconnectedRelays; + vector unconnectedRelays; for (string relay : relays) { bool isActive = find(this->_activeRelays.begin(), this->_activeRelays.end(), relay) diff --git a/test/nostr_service_test.cpp b/test/nostr_service_test.cpp index 0f0d439..b3b9b28 100644 --- a/test/nostr_service_test.cpp +++ b/test/nostr_service_test.cpp @@ -10,25 +10,10 @@ #include #include +using namespace std; +using namespace ::testing; + using nlohmann::json; -using std::function; -using std::lock_guard; -using std::make_shared; -using std::make_unique; -using std::mutex; -using std::promise; -using std::shared_ptr; -using std::string; -using std::thread; -using std::tuple; -using std::unordered_map; -using std::vector; -using ::testing::_; -using ::testing::Args; -using ::testing::HasSubstr; -using ::testing::Invoke; -using ::testing::Return; -using ::testing::Truly; namespace nostr_test { @@ -56,7 +41,7 @@ public: class NostrServiceTest : public testing::Test { public: - inline static const nostr::RelayList defaultTestRelays = + inline static const vector defaultTestRelays = { "wss://relay.damus.io", "wss://nostr.thesamecat.io" @@ -273,7 +258,7 @@ TEST_F(NostrServiceTest, OpenRelayConnections_OpensConnections_ToDefaultRelays) TEST_F(NostrServiceTest, OpenRelayConnections_OpensConnections_ToProvidedRelays) { - nostr::RelayList testRelays = { "wss://nos.lol" }; + vector testRelays = { "wss://nos.lol" }; mutex connectionStatusMutex; auto connectionStatus = make_shared>(); @@ -312,7 +297,7 @@ TEST_F(NostrServiceTest, OpenRelayConnections_OpensConnections_ToProvidedRelays) TEST_F(NostrServiceTest, OpenRelayConnections_AddsOpenConnections_ToActiveRelays) { - nostr::RelayList testRelays = { "wss://nos.lol" }; + vector testRelays = { "wss://nos.lol" }; mutex connectionStatusMutex; auto connectionStatus = make_shared>(); @@ -401,8 +386,8 @@ TEST_F(NostrServiceTest, CloseRelayConnections_ClosesConnections_ToActiveRelays) TEST_F(NostrServiceTest, CloseRelayConnections_RemovesClosedConnections_FromActiveRelays) { - nostr::RelayList testRelays = { "wss://nos.lol" }; - nostr::RelayList allTestRelays = { defaultTestRelays[0], defaultTestRelays[1], testRelays[0] }; + vector testRelays = { "wss://nos.lol" }; + vector allTestRelays = { defaultTestRelays[0], defaultTestRelays[1], testRelays[0] }; mutex connectionStatusMutex; auto connectionStatus = make_shared>(); -- cgit