From a57db2270f1a841325c47d5113befbb5ca532952 Mon Sep 17 00:00:00 2001 From: Michael Jurkoic Date: Tue, 12 Mar 2024 08:49:43 -0500 Subject: Match event.cpp formatting to rest of project --- src/event.cpp | 51 +++++++++++++++++++++++++-------------------------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/src/event.cpp b/src/event.cpp index 75f2ee8..0e4e159 100644 --- a/src/event.cpp +++ b/src/event.cpp @@ -1,38 +1,37 @@ -#pragma once - #include #include #include #include "nostr.hpp" +using nlohmann::json; using std::string; namespace nostr { - nlohmann::json Event::serialize() const - { - nlohmann::json j = { - {"id", this->id}, - {"pubkey", this->pubkey}, - {"created_at", this->created_at}, - {"kind", this->kind}, - {"tags", this->tags}, - {"content", this->content}, - {"sig", this->sig} - }; - return j.dump(); +json Event::serialize() const +{ + json j = { + {"id", this->id}, + {"pubkey", this->pubkey}, + {"created_at", this->created_at}, + {"kind", this->kind}, + {"tags", this->tags}, + {"content", this->content}, + {"sig", this->sig} }; + return j.dump(); +}; - void Event::deserialize(string jsonString) - { - nlohmann::json j = nlohmann::json::parse(jsonString); - this->id = j["id"]; - this->pubkey = j["pubkey"]; - this->created_at = j["created_at"]; - this->kind = j["kind"]; - this->tags = j["tags"]; - this->content = j["content"]; - this->sig = j["sig"]; - }; -} +void Event::deserialize(string jsonString) +{ + json j = json::parse(jsonString); + this->id = j["id"]; + this->pubkey = j["pubkey"]; + this->created_at = j["created_at"]; + this->kind = j["kind"]; + this->tags = j["tags"]; + this->content = j["content"]; + this->sig = j["sig"]; +}; +} // namespace nostr -- cgit From 6134935fd0c7adfb097824ea9e57207e3f97423b Mon Sep 17 00:00:00 2001 From: Michael Jurkoic Date: Tue, 12 Mar 2024 09:32:25 -0500 Subject: Add validation on Event serialization --- include/nostr.hpp | 22 ++++++++++++++++++++-- src/event.cpp | 52 ++++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 68 insertions(+), 6 deletions(-) diff --git a/include/nostr.hpp b/include/nostr.hpp index 47b56f9..ec8d1a8 100644 --- a/include/nostr.hpp +++ b/include/nostr.hpp @@ -27,14 +27,32 @@ struct Event { std::string id; ///< SHA-256 hash of the event data. std::string pubkey; ///< Public key of the event creator. - std::string created_at; ///< Unix timestamp of the event creation. + std::time_t createdAt; ///< Unix timestamp of the event creation. int kind; ///< Event kind. std::vector> tags; ///< Arbitrary event metadata. std::string content; ///< Event content. std::string sig; ///< Event signature created with the private key of the event creator. - nlohmann::json serialize() const; + /** + * @brief Serializes the event to a JSON object. + * @returns A stringified JSON object representing the event. + * @throws `std::invalid_argument` if the event object is invalid. + */ + std::string serialize(); + + /** + * @brief Deserializes the event from a JSON string. + * @param jsonString A stringified JSON object representing the event. + */ void deserialize(std::string jsonString); + +private: + /** + * @brief Validates the event. + * @throws `std::invalid_argument` if the event object is invalid. + * @remark The `createdAt` field defaults to the present if it is not already set. + */ + void validate(); }; class NostrService diff --git a/src/event.cpp b/src/event.cpp index 0e4e159..6a179fa 100644 --- a/src/event.cpp +++ b/src/event.cpp @@ -1,20 +1,31 @@ +#include #include -#include #include #include "nostr.hpp" using nlohmann::json; +using std::invalid_argument; using std::string; +using std::time; namespace nostr { -json Event::serialize() const +string Event::serialize() { + try + { + this->validate(); + } + catch (const invalid_argument& e) + { + throw e; + } + json j = { {"id", this->id}, {"pubkey", this->pubkey}, - {"created_at", this->created_at}, + {"created_at", this->createdAt}, {"kind", this->kind}, {"tags", this->tags}, {"content", this->content}, @@ -28,10 +39,43 @@ void Event::deserialize(string jsonString) json j = json::parse(jsonString); this->id = j["id"]; this->pubkey = j["pubkey"]; - this->created_at = j["created_at"]; + this->createdAt = j["created_at"]; this->kind = j["kind"]; this->tags = j["tags"]; this->content = j["content"]; this->sig = j["sig"]; }; + +void Event::validate() +{ + bool hasId = this->id.length() > 0; + if (!hasId) + { + throw std::invalid_argument("Event::validate: The event id is required."); + } + + bool hasPubkey = this->pubkey.length() > 0; + if (!hasPubkey) + { + throw std::invalid_argument("Event::validate: The pubkey of the event author is required."); + } + + bool hasCreatedAt = this->createdAt > 0; + if (!hasCreatedAt) + { + this->createdAt = time(nullptr); + } + + bool hasKind = this->kind >= 0 && this->kind < 40000; + if (!hasKind) + { + throw std::invalid_argument("Event::validate: A valid event kind is required."); + } + + bool hasSig = this->sig.length() > 0; + if (!hasSig) + { + throw std::invalid_argument("Event::validate: The event must be signed."); + } +}; } // namespace nostr -- cgit 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(-) 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 c564313f7e97f2fd7db98c2acca187c809a40a8c Mon Sep 17 00:00:00 2001 From: Michael Jurkoic Date: Tue, 12 Mar 2024 09:34:51 -0500 Subject: Add a filters struct for relay queries --- CMakeLists.txt | 1 + include/nostr.hpp | 33 +++++++++++++++++++++++++ src/filters.cpp | 74 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 108 insertions(+) create mode 100644 src/filters.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 3d472c1..4e46b70 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -22,6 +22,7 @@ set(SOURCE_DIR ./src) set(CLIENT_SOURCE_DIR ./src/client) set(SOURCES ${SOURCE_DIR}/event.cpp + ${SOURCE_DIR}/filters.cpp ${SOURCE_DIR}/nostr_service.cpp ${CLIENT_SOURCE_DIR}/websocketpp_client.cpp ) diff --git a/include/nostr.hpp b/include/nostr.hpp index ec8d1a8..ce25446 100644 --- a/include/nostr.hpp +++ b/include/nostr.hpp @@ -15,6 +15,7 @@ namespace nostr { typedef std::vector RelayList; +typedef std::unordered_map> TagMap; // TODO: Add null checking to seralization and deserialization methods. /** @@ -55,6 +56,38 @@ private: void validate(); }; +/** + * @brief A set of filters for querying Nostr relays. + * @remark The `limit` field should always be included to keep the response size reasonable. The + * `since` field is not required, and the `until` field will default to the present. At least one + * of the other fields must be set for a valid filter. + */ +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::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. + + /** + * @brief Serializes the filters to a JSON object. + * @returns A stringified JSON object representing the filters. + * @throws `std::invalid_argument` if the filter object is invalid. + */ + std::string serialize(); + +private: + /** + * @brief Validates the filters. + * @throws `std::invalid_argument` if the filter object is invalid. + * @remark The `until` field defaults to the present if it is not already set. + */ + void validate(); +}; + class NostrService { public: diff --git a/src/filters.cpp b/src/filters.cpp new file mode 100644 index 0000000..78f3ce4 --- /dev/null +++ b/src/filters.cpp @@ -0,0 +1,74 @@ +#include +#include +#include +#include + +#include "nostr.hpp" + +using nlohmann::json; +using std::invalid_argument; +using std::stringstream; +using std::string; +using std::time; + +namespace nostr +{ +string Filters::serialize() +{ + try + { + this->validate(); + } + catch (const invalid_argument& e) + { + throw e; + } + + json j = { + {"ids", this->ids}, + {"authors", this->authors}, + {"kinds", this->kinds}, + {"since", this->since}, + {"until", this->until}, + {"limit", this->limit} + }; + + for (auto& tag : this->tags) + { + stringstream jss; + jss << "#" << tag.first; + string js = jss.str(); + + j[js] = tag.second; + } + + return j.dump(); +}; + +void Filters::validate() +{ + bool hasLimit = this->limit > 0; + if (!hasLimit) + { + throw invalid_argument("Filters::validate: The limit must be greater than 0."); + } + + bool hasUntil = this->until > 0; + if (!hasUntil) + { + this->until = time(nullptr); + } + + bool hasIds = this->ids.size() > 0; + bool hasAuthors = this->authors.size() > 0; + bool hasKinds = this->kinds.size() > 0; + bool hasTags = this->tags.size() > 0; + + bool hasFilter = hasIds || hasAuthors || hasKinds || hasTags; + + if (!hasFilter) + { + throw invalid_argument("Filters::validate: At least one filter must be set."); + } +}; +} // namespace nostr -- cgit From a59ade344b54d658ba780b3f9e8979376371d653 Mon Sep 17 00:00:00 2001 From: Michael Jurkoic Date: Tue, 12 Mar 2024 09:51:04 -0500 Subject: Generate a valid ID while serializing an event --- include/nostr.hpp | 9 +++++++++ src/event.cpp | 36 +++++++++++++++++++++++++++++------- 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/include/nostr.hpp b/include/nostr.hpp index ce25446..fa407ef 100644 --- a/include/nostr.hpp +++ b/include/nostr.hpp @@ -54,6 +54,15 @@ private: * @remark The `createdAt` field defaults to the present if it is not already set. */ void validate(); + + /** + * @brief Generates an ID for the event. + * @param serializedData The serialized JSON string of all of the event data except the ID and + * the signature. + * @return A valid Nostr event ID. + * @remark The ID is a 32-bytes lowercase hex-encoded sha256 of the serialized event data. + */ + std::string generateId(std::string serializedData) const; }; /** diff --git a/src/event.cpp b/src/event.cpp index 6a179fa..a95657e 100644 --- a/src/event.cpp +++ b/src/event.cpp @@ -1,12 +1,19 @@ #include +#include +#include #include #include +#include #include "nostr.hpp" using nlohmann::json; +using std::hex; using std::invalid_argument; +using std::setw; +using std::setfill; using std::string; +using std::stringstream; using std::time; namespace nostr @@ -23,7 +30,6 @@ string Event::serialize() } json j = { - {"id", this->id}, {"pubkey", this->pubkey}, {"created_at", this->createdAt}, {"kind", this->kind}, @@ -31,6 +37,11 @@ string Event::serialize() {"content", this->content}, {"sig", this->sig} }; + + j["id"] = this->generateId(j.dump()); + + // TODO: Reach out to a signer to sign the event, then set the signature. + return j.dump(); }; @@ -48,12 +59,6 @@ void Event::deserialize(string jsonString) void Event::validate() { - bool hasId = this->id.length() > 0; - if (!hasId) - { - throw std::invalid_argument("Event::validate: The event id is required."); - } - bool hasPubkey = this->pubkey.length() > 0; if (!hasPubkey) { @@ -78,4 +83,21 @@ void Event::validate() throw std::invalid_argument("Event::validate: The event must be signed."); } }; + +string Event::generateId(string serializedData) const +{ + unsigned char hash[SHA256_DIGEST_LENGTH]; + SHA256_CTX sha256; + SHA256_Init(&sha256); + SHA256_Update(&sha256, serializedData.c_str(), serializedData.length()); + SHA256_Final(hash, &sha256); + + stringstream ss; + for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) + { + ss << hex << setw(2) << setfill('0') << (int)hash[i]; + } + + return ss.str(); +}; } // namespace nostr -- cgit From eb3044b06f6a8275a47478ddd5d8aa10a809422f Mon Sep 17 00:00:00 2001 From: Michael Jurkoic Date: Tue, 12 Mar 2024 09:51:24 -0500 Subject: Tweak variable names in Filters::serialize() --- src/filters.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/filters.cpp b/src/filters.cpp index 78f3ce4..0735a19 100644 --- a/src/filters.cpp +++ b/src/filters.cpp @@ -35,11 +35,11 @@ string Filters::serialize() for (auto& tag : this->tags) { - stringstream jss; - jss << "#" << tag.first; - string js = jss.str(); + stringstream ss; + ss << "#" << tag.first; + string tagname = ss.str(); - j[js] = tag.second; + j[tagname] = tag.second; } return j.dump(); -- cgit From ee5b3d683cc480c5eacc8cc0b9fc5f7a197901fd Mon Sep 17 00:00:00 2001 From: Michael Jurkoic Date: Wed, 13 Mar 2024 09:00:53 -0500 Subject: Use SHA256 hashing function for OpenSSL >= 3.0 --- src/event.cpp | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/event.cpp b/src/event.cpp index a95657e..eb6d998 100644 --- a/src/event.cpp +++ b/src/event.cpp @@ -1,9 +1,9 @@ #include +#include #include #include #include #include -#include #include "nostr.hpp" @@ -87,10 +87,7 @@ void Event::validate() string Event::generateId(string serializedData) const { unsigned char hash[SHA256_DIGEST_LENGTH]; - SHA256_CTX sha256; - SHA256_Init(&sha256); - SHA256_Update(&sha256, serializedData.c_str(), serializedData.length()); - SHA256_Final(hash, &sha256); + EVP_Digest(serializedData.c_str(), serializedData.length(), hash, NULL, EVP_sha256(), NULL); stringstream ss; for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) -- cgit From 81e3edba81f551d1c9129070390cf8411bec586b Mon Sep 17 00:00:00 2001 From: Michael Jurkoic Date: Sun, 17 Mar 2024 13:42:54 -0500 Subject: Include SHA header for events --- src/event.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/event.cpp b/src/event.cpp index eb6d998..2b8d4ed 100644 --- a/src/event.cpp +++ b/src/event.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include "nostr.hpp" -- cgit From 137b8b1267e1202b5d6f673bbee49526ae64aaab Mon Sep 17 00:00:00 2001 From: Michael Jurkoic Date: Sun, 17 Mar 2024 13:43:09 -0500 Subject: Send events as arrays --- src/event.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/event.cpp b/src/event.cpp index 2b8d4ed..4ba87d2 100644 --- a/src/event.cpp +++ b/src/event.cpp @@ -43,7 +43,9 @@ string Event::serialize() // TODO: Reach out to a signer to sign the event, then set the signature. - return j.dump(); + json jarr = json::array({ "EVENT", j }); + + return jarr.dump(); }; void Event::deserialize(string jsonString) -- cgit From 20b0f9c073d52e95b02399d6a243010e36b6c4f1 Mon Sep 17 00:00:00 2001 From: Michael Jurkoic Date: Sun, 17 Mar 2024 14:09:42 -0500 Subject: Serialize relay query filters into a JSON array --- include/nostr.hpp | 5 ++++- src/filters.cpp | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/include/nostr.hpp b/include/nostr.hpp index fa407ef..8041efe 100644 --- a/include/nostr.hpp +++ b/include/nostr.hpp @@ -83,10 +83,13 @@ struct Filters /** * @brief Serializes the filters to a JSON object. + * @param subscriptionId A string up to 64 chars in length that is unique per relay connection. * @returns A stringified JSON object representing the filters. * @throws `std::invalid_argument` if the filter object is invalid. + * @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 serialize(std::string subscriptionId); private: /** diff --git a/src/filters.cpp b/src/filters.cpp index 0735a19..3179c2f 100644 --- a/src/filters.cpp +++ b/src/filters.cpp @@ -13,7 +13,7 @@ using std::time; namespace nostr { -string Filters::serialize() +string Filters::serialize(string subscriptionId) { try { @@ -42,6 +42,8 @@ string Filters::serialize() j[tagname] = tag.second; } + json jarr = json::array({ "REQ", subscriptionId, j }); + return j.dump(); }; -- 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(-) 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(-) 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(+) 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 9832867f9ed9dbcc7ffbd135291bc54c153640b0 Mon Sep 17 00:00:00 2001 From: Michael Jurkoic Date: Sun, 17 Mar 2024 19:15:29 -0500 Subject: Define a receive method on the WebSocket interface --- include/client/web_socket_client.hpp | 9 +++++++++ test/nostr_service_test.cpp | 11 +++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/include/client/web_socket_client.hpp b/include/client/web_socket_client.hpp index 0f58749..4e6cb8b 100644 --- a/include/client/web_socket_client.hpp +++ b/include/client/web_socket_client.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include namespace client @@ -41,6 +42,14 @@ public: */ virtual std::tuple send(std::string message, std::string uri) = 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. + * @param messageHandler A callable object that will be invoked when the client receives a + * message from the server. + */ + virtual void receive(std::string uri, std::function messageHandler) = 0; + /** * @brief Closes the connection to the given server. */ diff --git a/test/nostr_service_test.cpp b/test/nostr_service_test.cpp index 83de3be..d4fc71b 100644 --- a/test/nostr_service_test.cpp +++ b/test/nostr_service_test.cpp @@ -7,11 +7,13 @@ #include #include +using std::function; using std::lock_guard; using std::make_shared; using std::mutex; using std::shared_ptr; using std::string; +using std::tuple; using std::unordered_map; using ::testing::_; using ::testing::Invoke; @@ -23,10 +25,11 @@ class MockWebSocketClient : public client::IWebSocketClient { public: MOCK_METHOD(void, start, (), (override)); MOCK_METHOD(void, stop, (), (override)); - MOCK_METHOD(void, openConnection, (std::string uri), (override)); - MOCK_METHOD(bool, isConnected, (std::string uri), (override)); - MOCK_METHOD((std::tuple), send, (std::string message, std::string uri), (override)); - MOCK_METHOD(void, closeConnection, (std::string uri), (override)); + 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, closeConnection, (string uri), (override)); }; class NostrServiceTest : public testing::Test -- 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(-) 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(-) 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(-) 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 08872f788a4a09f84b17e334afcd8973f1a0a346 Mon Sep 17 00:00:00 2001 From: Michael Jurkoic Date: Mon, 18 Mar 2024 20:51:02 -0500 Subject: Namespace pointer declaration --- include/nostr.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/nostr.hpp b/include/nostr.hpp index 51ea7cd..645090a 100644 --- a/include/nostr.hpp +++ b/include/nostr.hpp @@ -212,7 +212,7 @@ private: const int MAX_EVENTS_PER_SUBSCRIPTION = 128; ///< The WebSocket client used to communicate with relays. - shared_ptr _client; + std::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. -- cgit From a437d34b29d2a65113f3f67ffa1a6c3391b7e836 Mon Sep 17 00:00:00 2001 From: Michael Jurkoic Date: Mon, 18 Mar 2024 20:51:22 -0500 Subject: Implement receive method in WebSocket client --- src/client/websocketpp_client.cpp | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/client/websocketpp_client.cpp b/src/client/websocketpp_client.cpp index 1386e1a..5199343 100644 --- a/src/client/websocketpp_client.cpp +++ b/src/client/websocketpp_client.cpp @@ -1,9 +1,12 @@ +#include #include #include #include "web_socket_client.hpp" +using nlohmann::json; using std::error_code; +using std::function; using std::lock_guard; using std::make_tuple; using std::mutex; @@ -83,6 +86,29 @@ public: return make_tuple(uri, true); }; + void receive(string uri, function messageHandler) override + { + this->_client.set_message_handler([this, 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. + }); + }; + void closeConnection(string uri) override { lock_guard lock(this->_propertyMutex); @@ -103,5 +129,9 @@ private: websocketpp_client _client; unordered_map _connectionHandles; mutex _propertyMutex; + + void onMessage(websocketpp::connection_hdl handle, websocketpp_client::message_ptr message) + { + }; }; } // namespace client -- 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(-) 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(-) 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(-) 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(-) 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(-) 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(-) 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 d6adbc39741ebd79c5b41f20f7cd531171e620ec Mon Sep 17 00:00:00 2001 From: Michael Jurkoic Date: Sat, 30 Mar 2024 18:05:03 -0500 Subject: Test provided handlers in queryRelays --- test/nostr_service_test.cpp | 69 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 67 insertions(+), 2 deletions(-) diff --git a/test/nostr_service_test.cpp b/test/nostr_service_test.cpp index 6b68221..71af093 100644 --- a/test/nostr_service_test.cpp +++ b/test/nostr_service_test.cpp @@ -543,7 +543,7 @@ TEST_F(NostrServiceTest, QueryRelays_UsesDefaultHandler_AndReturnsSubscriptionId { *sentSubscriptionId = temp.substr(1, temp.size() - 2); } - + return make_tuple(uri, true); })); EXPECT_CALL(*mockClient, receive(_, _)) @@ -556,6 +556,71 @@ TEST_F(NostrServiceTest, QueryRelays_UsesDefaultHandler_AndReturnsSubscriptionId auto filters = make_shared(getTestFilters()); auto receivedSubscriptionId = nostrService->queryRelays(filters); - EXPECT_STREQ(receivedSubscriptionId.c_str(), sentSubscriptionId->c_str()); + ASSERT_STREQ(receivedSubscriptionId.c_str(), sentSubscriptionId->c_str()); +}; + +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) + { + messageHandler(getTestEventMessage(*sentSubscriptionId)); + })); + + auto filters = make_shared(getTestFilters()); + nostr::Event expectedEvent = getTestEvent(); + 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 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(-) 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(-) 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(-) 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(-) 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(-) 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(-) 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 ed539e09128b88080de9e2b31fda931cbc5c0399 Mon Sep 17 00:00:00 2001 From: Michael Jurkoic Date: Sun, 14 Apr 2024 23:15:38 -0500 Subject: Unit test rejected events --- test/nostr_service_test.cpp | 118 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 115 insertions(+), 3 deletions(-) diff --git a/test/nostr_service_test.cpp b/test/nostr_service_test.cpp index cd6307b..c408010 100644 --- a/test/nostr_service_test.cpp +++ b/test/nostr_service_test.cpp @@ -519,6 +519,114 @@ TEST_F(NostrServiceTest, PublishEvent_CorrectlyIndicates_MixedSuccessesAndFailur defaultTestRelays); nostrService->openRelayConnections(); + // 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, function messageHandler) + { + return make_tuple(uri, false); + })); + EXPECT_CALL(*mockClient, send(_, defaultTestRelays[1], _)) + .Times(1) + .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); + })); + + auto testEvent = make_shared(getTextNoteTestEvent()); + auto [successes, failures] = nostrService->publishEvent(testEvent); + + ASSERT_EQ(successes.size(), 1); + ASSERT_EQ(successes[0], defaultTestRelays[1]); + + ASSERT_EQ(failures.size(), 1); + ASSERT_EQ(failures[0], defaultTestRelays[0]); +}; + +// TODO: Add unit tests for events rejected by relays. +TEST_F(NostrServiceTest, PublishEvent_CorrectlyIndicates_RejectedEvent) +{ + 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(); + + // Simulate a scenario where the message is rejected by all target relays. + EXPECT_CALL(*mockClient, send(_, _, _)) + .Times(2) + .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, false, "Event rejected" }); + messageHandler(jarr.dump()); + + return make_tuple(uri, true); + })); + + auto testEvent = make_shared(getTextNoteTestEvent()); + auto [successes, failures] = nostrService->publishEvent(testEvent); + + ASSERT_EQ(failures.size(), defaultTestRelays.size()); + for (auto relay : failures) + { + ASSERT_NE(find(defaultTestRelays.begin(), defaultTestRelays.end(), relay), defaultTestRelays.end()); + } +}; + +TEST_F(NostrServiceTest, PublishEvent_CorrectlyIndicates_EventRejectedBySomeRelays) +{ + 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(); + // 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], _)) @@ -537,7 +645,13 @@ TEST_F(NostrServiceTest, PublishEvent_CorrectlyIndicates_MixedSuccessesAndFailur .Times(1) .WillRepeatedly(Invoke([](string message, string uri, function messageHandler) { - return make_tuple(uri, false); + json messageArr = json::parse(message); + auto event = nostr::Event::fromString(messageArr[1]); + + json jarr = json::array({ "OK", event.id, false, "Event rejected" }); + messageHandler(jarr.dump()); + + return make_tuple(uri, true); })); auto testEvent = make_shared(getTextNoteTestEvent()); @@ -550,8 +664,6 @@ TEST_F(NostrServiceTest, PublishEvent_CorrectlyIndicates_MixedSuccessesAndFailur 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. -- cgit From 164eaab23e36be7300b740d1a0e37155c02755ff Mon Sep 17 00:00:00 2001 From: Michael Jurkoic Date: Mon, 15 Apr 2024 09:55:16 -0500 Subject: Add unit tests for batch queries Also add an equality operator for nostr::Event --- include/nostr.hpp | 8 +++ src/event.cpp | 14 +++++ test/nostr_service_test.cpp | 132 +++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 153 insertions(+), 1 deletion(-) diff --git a/include/nostr.hpp b/include/nostr.hpp index 62eceff..5c99bf4 100644 --- a/include/nostr.hpp +++ b/include/nostr.hpp @@ -58,6 +58,14 @@ struct Event */ static Event fromJson(nlohmann::json j); + /** + * @brief Compares two events for equality. + * @remark Two events are considered equal if they have the same ID, since the ID is uniquely + * generated from the event data. If the `id` field is empty for either event, the comparison + * function will throw an exception. + */ + bool operator==(const Event& other) const; + private: /** * @brief Validates the event. diff --git a/src/event.cpp b/src/event.cpp index 7b5bfb2..347065c 100644 --- a/src/event.cpp +++ b/src/event.cpp @@ -125,4 +125,18 @@ string Event::generateId(string serializedData) const return ss.str(); }; + +bool Event::operator==(const Event& other) const +{ + if (this->id.empty()) + { + throw invalid_argument("Event::operator==: Cannot check equality, the left-side argument is undefined."); + } + if (other.id.empty()) + { + throw invalid_argument("Event::operator==: Cannot check equality, the right-side argument is undefined."); + } + + return this->id == other.id; +}; } // namespace nostr diff --git a/test/nostr_service_test.cpp b/test/nostr_service_test.cpp index c408010..d23f1bb 100644 --- a/test/nostr_service_test.cpp +++ b/test/nostr_service_test.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -19,6 +20,7 @@ using std::shared_ptr; using std::string; using std::tuple; using std::unordered_map; +using std::vector; using ::testing::_; using ::testing::Args; using ::testing::Invoke; @@ -73,6 +75,50 @@ public: return event; }; + static const vector getMultipleTextNoteTestEvents() + { + auto now = std::chrono::system_clock::now(); + std::time_t currentTime = std::chrono::system_clock::to_time_t(now); + + nostr::Event event1; + event1.pubkey = "13tn5ccv2guflxgffq4aj0hw5x39pz70zcdrfd6vym887gry38zys28dask"; + event1.kind = 1; + event1.tags = + { + { "e", "5c83da77af1dec6d7289834998ad7aafbd9e2191396d75ec3cc27f5a77226f36", "wss://nostr.example.com" }, + { "p", "f7234bd4c1394dda46d09f35bd384dd30cc552ad5541990f98844fb06676e9ca" }, + { "a", "30023:f7234bd4c1394dda46d09f35bd384dd30cc552ad5541990f98844fb06676e9ca:abcd", "wss://nostr.example.com" } + }; + event1.content = "Hello, World!"; + event1.createdAt = currentTime; + + nostr::Event event2; + event2.pubkey = "1l9d9jh67rkwayalrxcy686aujyz5pper5kzjv8jvg8pu9v9ns4ls0xvq42"; + event2.kind = 1; + event2.tags = + { + { "e", "5c83da77af1dec6d7289834998ad7aafbd9e2191396d75ec3cc27f5a77226f36", "wss://nostr.example.com" }, + { "p", "f7234bd4c1394dda46d09f35bd384dd30cc552ad5541990f98844fb06676e9ca" }, + { "a", "30023:f7234bd4c1394dda46d09f35bd384dd30cc552ad5541990f98844fb06676e9ca:abcd", "wss://nostr.example.com" } + }; + event2.content = "Welcome to Nostr!"; + event2.createdAt = currentTime; + + nostr::Event event3; + event3.pubkey = "187ujhtmnv82ftg03h4heetwk3dd9mlfkf8th3fvmrk20nxk9mansuzuyla"; + event3.kind = 1; + event3.tags = + { + { "e", "5c83da77af1dec6d7289834998ad7aafbd9e2191396d75ec3cc27f5a77226f36", "wss://nostr.example.com" }, + { "p", "f7234bd4c1394dda46d09f35bd384dd30cc552ad5541990f98844fb06676e9ca" }, + { "a", "30023:f7234bd4c1394dda46d09f35bd384dd30cc552ad5541990f98844fb06676e9ca:abcd", "wss://nostr.example.com" } + }; + event3.content = "Time for some introductions!"; + event3.createdAt = currentTime; + + return { event1, event2, event3 }; + }; + static const nostr::Event getLongFormTestEvent() { nostr::Event event; @@ -550,7 +596,6 @@ TEST_F(NostrServiceTest, PublishEvent_CorrectlyIndicates_MixedSuccessesAndFailur ASSERT_EQ(failures[0], defaultTestRelays[0]); }; -// TODO: Add unit tests for events rejected by relays. TEST_F(NostrServiceTest, PublishEvent_CorrectlyIndicates_RejectedEvent) { mutex connectionStatusMutex; @@ -665,6 +710,91 @@ TEST_F(NostrServiceTest, PublishEvent_CorrectlyIndicates_EventRejectedBySomeRela }; // TODO: Add unit tests for queries. +TEST_F(NostrServiceTest, QueryRelays_ReturnsEvents_UpToEOSE) +{ + 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()); + auto results = nostrService->queryRelays(filters); + + // TODO: Check results size when the queryRelays method deduplicates results before returning. + // ASSERT_EQ(results.size(), testEvents.size()); + + // Check that the results contain the expected events. + for (auto resultEvent : results) + { + ASSERT_NE( + find_if( + signedTestEvents.begin(), + signedTestEvents.end(), + [&resultEvent](shared_ptr testEvent) + { + return *testEvent == *resultEvent; + }), + signedTestEvents.end()); + } +}; // TODO: Add unit tests for closing subscriptions. } // namespace nostr_test -- cgit From c625dfa1a04cb0f0bed5e51bac64b595c8b483c3 Mon Sep 17 00:00:00 2001 From: Finrod Felagund Date: Mon, 15 Apr 2024 22:24:49 +0200 Subject: configure vcpkg build --- CMakePresets.json | 13 +++++++++++++ vcpkg-configuration.json | 14 ++++++++++++++ vcpkg.json | 8 ++++++++ 3 files changed, 35 insertions(+) create mode 100644 CMakePresets.json create mode 100644 vcpkg-configuration.json create mode 100644 vcpkg.json diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 0000000..8c57178 --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,13 @@ +{ + "version": 2, + "configurePresets": [ + { + "name": "default", + "generator": "Ninja", + "binaryDir": "${sourceDir}/build", + "cacheVariables": { + "CMAKE_TOOLCHAIN_FILE": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" + } + } + ] + } \ No newline at end of file diff --git a/vcpkg-configuration.json b/vcpkg-configuration.json new file mode 100644 index 0000000..850abfe --- /dev/null +++ b/vcpkg-configuration.json @@ -0,0 +1,14 @@ +{ + "default-registry": { + "kind": "git", + "baseline": "582a4de14bef91df217f4f49624cf5b2b04bd7ca", + "repository": "https://github.com/microsoft/vcpkg" + }, + "registries": [ + { + "kind": "artifact", + "location": "https://github.com/microsoft/vcpkg-ce-catalog/archive/refs/heads/main.zip", + "name": "microsoft" + } + ] +} diff --git a/vcpkg.json b/vcpkg.json new file mode 100644 index 0000000..579ca88 --- /dev/null +++ b/vcpkg.json @@ -0,0 +1,8 @@ +{ + "dependencies": [ + "nlohmann-json", + "openssl", + "plog", + "websocketpp" + ] +} -- cgit From 357b6813b908c3f9b272243c9e99a5bc1b442c89 Mon Sep 17 00:00:00 2001 From: Finrod Felagund Date: Mon, 15 Apr 2024 22:26:02 +0200 Subject: fix include sha.h --- src/event.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/event.cpp b/src/event.cpp index 347065c..2df1c3a 100644 --- a/src/event.cpp +++ b/src/event.cpp @@ -4,7 +4,7 @@ #include #include #include -#include +#include #include "nostr.hpp" @@ -20,7 +20,7 @@ using std::string; using std::stringstream; using std::time; -namespace nostr +namespace nostr { string Event::serialize() { -- 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(-) 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 eca45873a1adbfac46a05fd05199b30a0ce3b6b9 Mon Sep 17 00:00:00 2001 From: Finrod Felagund Date: Mon, 15 Apr 2024 22:54:16 +0200 Subject: put binary outputs at the level of GitRepublic --- CMakeLists.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 0610f3f..ca378b8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,9 +5,9 @@ project(NostrSDK VERSION 0.0.1) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) -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 (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) -- 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(-) 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 21aff33f4f194148f768cce28a7ef9c827af29e9 Mon Sep 17 00:00:00 2001 From: Michael Jurkoic Date: Tue, 23 Apr 2024 10:25:27 -0500 Subject: Begin switching to FetchContent for deps --- .gitignore | 9 +++++ CMakeLists.txt | 77 ++++++++++++++++++++++++++++-------- README.md | 14 +++++++ include/client/web_socket_client.hpp | 3 ++ include/nostr.hpp | 3 ++ src/client/websocketpp_client.cpp | 3 -- src/event.cpp | 3 -- src/filters.cpp | 3 +- 8 files changed, 91 insertions(+), 24 deletions(-) diff --git a/.gitignore b/.gitignore index 8138b0a..efb998a 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,12 @@ build/ # VS Code Settings .vscode/ + +# CMake outputs +_deps/ +CMakeFiles/ +Makefile +CTestTestfile.cmake +CMakeCache.txt +cmake_install.cmake +OpenSSL-prefix diff --git a/CMakeLists.txt b/CMakeLists.txt index ca378b8..208265a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,21 +1,73 @@ cmake_minimum_required(VERSION 3.14) +cmake_policy(SET CMP0135 NEW) project(NostrSDK VERSION 0.0.1) +include(ExternalProject) +include(FetchContent) + # Specify the C++ standard set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) -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/) +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.") + + 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(CMAKE_INSTALL_PREFIX ${CMAKE_SOURCE_DIR}/../env/) + + set(Boost_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/../env/boost/include) +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/uuid_v4) + list(APPEND CMAKE_PREFIX_PATH $ENV{WORKSPACE}/env) else() - list(APPEND CMAKE_PREFIX_PATH ../env/uuid_v4) + list(APPEND CMAKE_PREFIX_PATH ${CMAKE_SOURCE_DIR}/../env) endif() -# Build the project. +#======== Find unmanaged dependencies ========# +find_package(OpenSSL REQUIRED) +find_package(Boost REQUIRED COMPONENTS filesystem system thread regex) + +#======== 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 +) +FetchContent_Declare( + uuid_v4 + URL https://github.com/crashoz/uuid_v4/archive/refs/tags/v1.0.0.tar.gz + USES_TERMINAL_DOWNLOAD TRUE +) + +FetchContent_MakeAvailable(nlohmann_json plog uuid_v4 websocketpp) + +#======== Build the project ========# set(INCLUDE_DIR ./include) set(CLIENT_INCLUDE_DIR ./include/client) include_directories(${INCLUDE_DIR}) @@ -35,28 +87,21 @@ set(SOURCES ${CLIENT_SOURCE_DIR}/websocketpp_client.cpp ) -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 nlohmann_json::nlohmann_json OpenSSL::SSL OpenSSL::Crypto plog::plog - websocketpp::websocketpp uuid_v4::uuid_v4 + websocketpp::websocketpp ) set_target_properties(NostrSDK PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS YES) -# Build the tests. +#======== Build the tests ========# enable_testing() include(GoogleTest) -include(FetchContent) FetchContent_Declare( googletest URL https://github.com/google/googletest/archive/03597a01ee50ed33e9dfd640b249b4be3799d395.zip @@ -79,8 +124,8 @@ target_link_libraries(NostrSDKTest PRIVATE GTest::gtest_main NostrSDK plog::plog - websocketpp::websocketpp uuid_v4::uuid_v4 + websocketpp::websocketpp ) set_target_properties(NostrSDKTest PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS YES) diff --git a/README.md b/README.md index 4445cee..07ef788 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,16 @@ # NostrSDK + C++ System Development Kit for Nostr + +## Building the SDK + +### Prerequisites + +- CMake 3.14 or later +- C++17 compiler +- OpenSSL 1.1.1 or later +- Boost 1.85 or later + +### Standalone Build + +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. diff --git a/include/client/web_socket_client.hpp b/include/client/web_socket_client.hpp index 6fbede6..63fa634 100644 --- a/include/client/web_socket_client.hpp +++ b/include/client/web_socket_client.hpp @@ -3,6 +3,9 @@ #include #include +#include +#include + namespace client { /** diff --git a/include/nostr.hpp b/include/nostr.hpp index 5c99bf4..d6d5de1 100644 --- a/include/nostr.hpp +++ b/include/nostr.hpp @@ -6,6 +6,9 @@ #include #include +#include +#include + #include #include #include diff --git a/src/client/websocketpp_client.cpp b/src/client/websocketpp_client.cpp index 276c5dd..baae054 100644 --- a/src/client/websocketpp_client.cpp +++ b/src/client/websocketpp_client.cpp @@ -1,6 +1,3 @@ -#include -#include - #include "web_socket_client.hpp" using std::error_code; diff --git a/src/event.cpp b/src/event.cpp index cf6b117..5c98028 100644 --- a/src/event.cpp +++ b/src/event.cpp @@ -1,7 +1,4 @@ #include -#include -#include -#include #include "nostr.hpp" diff --git a/src/filters.cpp b/src/filters.cpp index af9960c..cfbc5bf 100644 --- a/src/filters.cpp +++ b/src/filters.cpp @@ -1,6 +1,5 @@ -#include - #include "nostr.hpp" + using namespace nlohmann; using namespace std; -- 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(-) 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 2f5c62d0ef2c31366fd2295184aa97e6f07e4ddd Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Sun, 28 Apr 2024 12:39:35 -0500 Subject: README instructions for testing --- .gitignore | 1 + README.md | 8 +++++++- vcpkg-configuration.json | 28 ++++++++++++++-------------- 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/.gitignore b/.gitignore index 6623964..5cf2f9a 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,7 @@ build/ _deps/ CMakeFiles/ out/ +Testing/ Makefile CTestTestfile.cmake CMakeCache.txt diff --git a/README.md b/README.md index b38e83f..de4d3ad 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ This project uses CMake as its build system, and vcpkg as its dependency manager - C++17 compiler - vcpkg -### Build Targets +### Building and Testing The SDK aims to support Linux, Windows, and macOS build targets. CMake presets are provided for each target. @@ -24,3 +24,9 @@ To build the SDK on Linux, run the following commands from the project root: cmake --preset=linux . cmake --build ./build/linux ``` + +To run unit tests, use the following command: + +```bash +ctest ./build/linux +``` diff --git a/vcpkg-configuration.json b/vcpkg-configuration.json index 850abfe..4b6ae85 100644 --- a/vcpkg-configuration.json +++ b/vcpkg-configuration.json @@ -1,14 +1,14 @@ -{ - "default-registry": { - "kind": "git", - "baseline": "582a4de14bef91df217f4f49624cf5b2b04bd7ca", - "repository": "https://github.com/microsoft/vcpkg" - }, - "registries": [ - { - "kind": "artifact", - "location": "https://github.com/microsoft/vcpkg-ce-catalog/archive/refs/heads/main.zip", - "name": "microsoft" - } - ] -} +{ + "default-registry": { + "kind": "git", + "baseline": "582a4de14bef91df217f4f49624cf5b2b04bd7ca", + "repository": "https://github.com/microsoft/vcpkg" + }, + "registries": [ + { + "kind": "artifact", + "location": "https://github.com/microsoft/vcpkg-ce-catalog/archive/refs/heads/main.zip", + "name": "microsoft" + } + ] +} -- cgit From b05e6adca19038f2d4efdd41df030a890344ef5a Mon Sep 17 00:00:00 2001 From: Michael Jurkoic Date: Tue, 30 Apr 2024 00:19:21 -0500 Subject: Rename project to 'aedile' --- CMakeLists.txt | 18 +++++++++--------- README.md | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4c521c8..2ed34df 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.19) cmake_policy(SET CMP0135 NEW) -project(NostrSDK VERSION 0.1.0) +project(aedile VERSION 0.0.2) include(ExternalProject) include(FetchContent) @@ -69,15 +69,15 @@ set(SOURCES ${CLIENT_SOURCE_DIR}/websocketpp_client.cpp ) -add_library(NostrSDK ${SOURCES} ${HEADERS}) -target_link_libraries(NostrSDK PRIVATE +add_library(aedile ${SOURCES} ${HEADERS}) +target_link_libraries(aedile PRIVATE nlohmann_json::nlohmann_json OpenSSL::SSL OpenSSL::Crypto plog::plog websocketpp::websocketpp ) -set_target_properties(NostrSDK PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS YES) +set_target_properties(aedile PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS YES) #======== Build the tests ========# enable_testing() @@ -98,15 +98,15 @@ set(TEST_SOURCES ${TEST_DIR}/nostr_service_test.cpp ) -add_executable(NostrSDKTest ${TEST_SOURCES} ${HEADERS}) -target_link_libraries(NostrSDKTest PRIVATE +add_executable(aedile_test ${TEST_SOURCES} ${HEADERS}) +target_link_libraries(aedile_test PRIVATE GTest::gmock GTest::gtest GTest::gtest_main - NostrSDK + aedile plog::plog websocketpp::websocketpp ) -set_target_properties(NostrSDKTest PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS YES) +set_target_properties(aedile_test PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS YES) -gtest_add_tests(TARGET NostrSDKTest) +gtest_add_tests(TARGET aedile_test) diff --git a/README.md b/README.md index de4d3ad..8388da6 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# NostrSDK +# Aedile C++ System Development Kit for Nostr -- 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(-) 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 a679bb3d635374cf60df3453f9dd96ddfca3f273 Mon Sep 17 00:00:00 2001 From: Michael Jurkoic Date: Tue, 30 Apr 2024 00:20:19 -0500 Subject: Add build and test presets for CMake --- CMakePresets.json | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CMakePresets.json b/CMakePresets.json index 3e327f5..d28f1a5 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -9,5 +9,18 @@ "CMAKE_TOOLCHAIN_FILE": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" } } + ], + "buildPresets": [ + { + "name": "linux", + "configurePreset": "linux", + "jobs": 4 + } + ], + "testPresets": [ + { + "name": "linux", + "configurePreset": "linux" + } ] } -- cgit From 1291f9b045adfea3279cc26531b5d13164ca1bd8 Mon Sep 17 00:00:00 2001 From: Michael Jurkoic Date: Tue, 30 Apr 2024 00:20:33 -0500 Subject: Give example commands for build and test --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 8388da6..f186fe2 100644 --- a/README.md +++ b/README.md @@ -21,12 +21,12 @@ The SDK aims to support Linux, Windows, and macOS build targets. CMake presets To build the SDK on Linux, run the following commands from the project root: ```bash -cmake --preset=linux . -cmake --build ./build/linux +cmake --preset linux +cmake --build --preset linux ``` To run unit tests, use the following command: ```bash -ctest ./build/linux +ctest --preset linux ``` -- 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(-) 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(-) 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(-) 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 80885e4f6b83a3a63b9a74640a13a66cc27d7933 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Mon, 6 May 2024 09:04:57 -0500 Subject: Add details to README --- README.md | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f186fe2..e9189dc 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,16 @@ # Aedile -C++ System Development Kit for Nostr +A Nostr System Development Kit written in C++. + +## Behind the Name + +In the ancient Roman Republic, the aediles were officials elected from among the plebians and charged with caring for Rome's public infrastructure and ensuring an accurate system of weights and measures. + +The aim of the Aedile SDK is in the spirit of that ancient office: + +- Provide a fast and efficient service for interacting with Nostr relays via WebSocket connections. +- Offer stable, well-tested implementations of commonly-used [Nostr Implementation Possibilities (NIPs)](https://github.com/nostr-protocol/nips/tree/master). +- Open up Nostr development by taking care of the basics so developers can focus on solving problems, rather than reimplementing the protocol. ## Building the SDK @@ -9,19 +19,20 @@ C++ System Development Kit for Nostr 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 +- A C++17 compiler - vcpkg +CMake invokes vcpkg at the start of the configure process to install some of the project's dependencies. For this step to succeed, ensure that the `VCPKG_ROOT` environment variable is set to the path of your vcpkg installation. + ### Building and Testing -The SDK aims to support Linux, Windows, and macOS build targets. CMake presets are provided for each target. +The SDK aims to support Linux, Windows, and macOS build targets. It currently supplies a CMake preset for Linux. #### Linux To build the SDK on Linux, run the following commands from the project root: ```bash -cmake --preset linux cmake --build --preset linux ``` -- 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(-) 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(-) 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