diff options
-rw-r--r-- | CMakeLists.txt | 29 | ||||
-rw-r--r-- | include/client/web_socket_client.hpp | 3 | ||||
-rw-r--r-- | include/data/data.hpp | 114 | ||||
-rw-r--r-- | include/nostr.hpp | 130 | ||||
-rw-r--r-- | include/signer/signer.hpp | 31 | ||||
-rw-r--r-- | src/client/websocketpp_client.cpp | 3 | ||||
-rw-r--r-- | src/data/event.cpp (renamed from src/event.cpp) | 7 | ||||
-rw-r--r-- | src/data/filters.cpp (renamed from src/filters.cpp) | 5 | ||||
-rw-r--r-- | src/nostr_service.cpp | 39 | ||||
-rw-r--r-- | src/signer/noscrypt_signer.cpp | 34 | ||||
-rw-r--r-- | test/nostr_service_test.cpp | 135 |
11 files changed, 319 insertions, 211 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index 754b0f9..e8be787 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,7 +8,8 @@ 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") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -march=native") +set(CMAKE_POSITION_INDEPENDENT_CODE ON) get_directory_property(HAS_PARENT PARENT_DIRECTORY) if(HAS_PARENT) @@ -45,39 +46,50 @@ FetchContent_Declare( GIT_TAG v1.0.0 ) FetchContent_Populate(uuid_v4) -set(uuid_v4_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/_deps/uuid_v4-src/) -find_path(uuid_v4_INCLUDE_DIR uuid_v4.h) -include_directories(${uuid_v4_INCLUDE_DIR}) +FetchContent_GetProperties(uuid_v4) +include_directories(${uuid_v4_SOURCE_DIR}) #======== Configure noscrypt ========# FetchContent_Declare( libnoscrypt GIT_REPOSITORY git@github.com:VnUgE/noscrypt.git - GIT_TAG 872c49d1925b2576f85ec2587747119e895a675b + GIT_TAG d09d9330415d463ca19be9394b02ce11b3366f7e ) FetchContent_MakeAvailable(libnoscrypt) +FetchContent_GetProperties(libnoscrypt) +include_directories(${libnoscrypt_SOURCE_DIR}/include) + set_target_properties(noscrypt PROPERTIES CRYPTO_LIB openssl) #======== Build the project ========# set(INCLUDE_DIR ./include) set(CLIENT_INCLUDE_DIR ./include/client) +set(DATA_INCLUDE_DIR ./include/data) +set(SIGNER_INCLUDE_DIR ./include/signer) include_directories(${INCLUDE_DIR}) include_directories(${CLIENT_INCLUDE_DIR}) +include_directories(${DATA_INCLUDE_DIR}) +include_directories(${SIGNER_INCLUDE_DIR}) set(HEADERS ${INCLUDE_DIR}/nostr.hpp ${CLIENT_INCLUDE_DIR}/web_socket_client.hpp + ${DATA_INCLUDE_DIR}/data.hpp + ${SIGNER_INCLUDE_DIR}/signer.hpp ) set(SOURCE_DIR ./src) set(CLIENT_SOURCE_DIR ./src/client) +set(DATA_SOURCE_DIR ./src/data) +set(SIGNER_SOURCE_DIR ./src/signer) set(SOURCES - ${SOURCE_DIR}/event.cpp - ${SOURCE_DIR}/filters.cpp ${SOURCE_DIR}/nostr_service.cpp ${CLIENT_SOURCE_DIR}/websocketpp_client.cpp + ${DATA_SOURCE_DIR}/event.cpp + ${DATA_SOURCE_DIR}/filters.cpp + ${SIGNER_SOURCE_DIR}/noscrypt_signer.cpp ) add_library(aedile ${SOURCES} ${HEADERS}) @@ -87,6 +99,7 @@ target_link_libraries(aedile PRIVATE OpenSSL::Crypto plog::plog websocketpp::websocketpp + noscrypt ) set_target_properties(aedile PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS YES) @@ -111,12 +124,12 @@ set(TEST_SOURCES add_executable(aedile_test ${TEST_SOURCES} ${HEADERS}) target_link_libraries(aedile_test PRIVATE - aedile GTest::gmock GTest::gtest GTest::gtest_main plog::plog websocketpp::websocketpp + aedile ) set_target_properties(aedile_test PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS YES) diff --git a/include/client/web_socket_client.hpp b/include/client/web_socket_client.hpp index 63fa634..e186e90 100644 --- a/include/client/web_socket_client.hpp +++ b/include/client/web_socket_client.hpp @@ -6,6 +6,8 @@ #include <websocketpp/client.hpp> #include <websocketpp/config/asio_client.hpp> +namespace nostr +{ namespace client { /** @@ -72,3 +74,4 @@ public: virtual void closeConnection(std::string uri) = 0; }; } // namespace client +} // namespace nostr diff --git a/include/data/data.hpp b/include/data/data.hpp new file mode 100644 index 0000000..46156cd --- /dev/null +++ b/include/data/data.hpp @@ -0,0 +1,114 @@ +#pragma once + +#include <chrono> +#include <string> +#include <unordered_map> +#include <vector> + +#include <nlohmann/json.hpp> +#include <openssl/evp.h> +#include <openssl/sha.h> + +namespace nostr +{ +namespace data +{ +/** + * @brief A Nostr event. + * @remark All data transmitted over the Nostr protocol is encoded in JSON blobs. This struct + * is common to every Nostr event kind. The significance of each event is determined by the + * `tags` and `content` fields. +*/ +struct Event +{ + std::string id; ///< SHA-256 hash of the event data. + std::string pubkey; ///< Public key of the event creator. + std::time_t createdAt; ///< Unix timestamp of the event creation. + int kind; ///< Event kind. + std::vector<std::vector<std::string>> tags; ///< Arbitrary event metadata. + std::string content; ///< Event content. + std::string sig; ///< Event signature created with the private key of the event creator. + + /** + * @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. + * @returns An event instance created from the JSON string. + */ + 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); + + /** + * @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. + * @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(); + + /** + * @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; +}; + +/** + * @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<std::string> ids; ///< Event IDs. + std::vector<std::string> authors; ///< Event author npubs. + std::vector<int> kinds; ///< Kind numbers. + std::unordered_map<std::string, std::vector<std::string>> 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. + * @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& subscriptionId); + +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(); +}; +} // namespace data +} // namespace nostr diff --git a/include/nostr.hpp b/include/nostr.hpp index e5b29c7..76bacd9 100644 --- a/include/nostr.hpp +++ b/include/nostr.hpp @@ -9,8 +9,6 @@ #include <vector> #include <nlohmann/json.hpp> -#include <openssl/evp.h> -#include <openssl/sha.h> #include <plog/Init.h> #include <plog/Log.h> #include <websocketpp/client.hpp> @@ -18,121 +16,24 @@ #include <uuid_v4.h> #include "client/web_socket_client.hpp" +#include "data/data.hpp" +#include "signer/signer.hpp" namespace nostr { -class ISigner; class NostrService; -/** - * @brief A Nostr event. - * @remark All data transmitted over the Nostr protocol is encoded in JSON blobs. This struct - * is common to every Nostr event kind. The significance of each event is determined by the - * `tags` and `content` fields. -*/ -struct Event -{ - std::string id; ///< SHA-256 hash of the event data. - std::string pubkey; ///< Public key of the event creator. - std::time_t createdAt; ///< Unix timestamp of the event creation. - int kind; ///< Event kind. - std::vector<std::vector<std::string>> tags; ///< Arbitrary event metadata. - std::string content; ///< Event content. - std::string sig; ///< Event signature created with the private key of the event creator. - - /** - * @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. - * @returns An event instance created from the JSON string. - */ - 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); - - /** - * @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. - * @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(); - - /** - * @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; -}; - -/** - * @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<std::string> ids; ///< Event IDs. - std::vector<std::string> authors; ///< Event author npubs. - std::vector<int> kinds; ///< Kind numbers. - std::unordered_map<std::string, std::vector<std::string>> 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. - * @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& subscriptionId); - -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: NostrService( std::shared_ptr<plog::IAppender> appender, std::shared_ptr<client::IWebSocketClient> client, - std::shared_ptr<ISigner> signer); + std::shared_ptr<signer::ISigner> signer); NostrService( std::shared_ptr<plog::IAppender> appender, std::shared_ptr<client::IWebSocketClient> client, - std::shared_ptr<ISigner> signer, + std::shared_ptr<signer::ISigner> signer, std::vector<std::string> relays); ~NostrService(); @@ -171,7 +72,7 @@ public: * to which relays the event was published successfully, and to which relays the event failed * to publish. */ - std::tuple<std::vector<std::string>, std::vector<std::string>> publishEvent(std::shared_ptr<Event> event); + std::tuple<std::vector<std::string>, std::vector<std::string>> publishEvent(std::shared_ptr<data::Event> event); /** * @brief Queries all open relay connections for events matching the given set of filters, and @@ -185,7 +86,7 @@ public: * set on the filters in the range 1-64, inclusive. If no valid limit is given, it will be * defaulted to 16. */ - std::vector<std::shared_ptr<Event>> queryRelays(std::shared_ptr<Filters> filters); + std::vector<std::shared_ptr<data::Event>> queryRelays(std::shared_ptr<data::Filters> filters); /** * @brief Queries all open relay connections for events matching the given set of filters. @@ -202,8 +103,8 @@ public: * events, and they will not be accessible via `getNewEvents`. */ std::string queryRelays( - std::shared_ptr<Filters> filters, - std::function<void(const std::string&, std::shared_ptr<Event>)> eventHandler, + std::shared_ptr<data::Filters> filters, + std::function<void(const std::string&, std::shared_ptr<data::Event>)> eventHandler, std::function<void(const std::string&)> eoseHandler, std::function<void(const std::string&, const std::string&)> closeHandler); @@ -242,7 +143,7 @@ private: ///< The WebSocket client used to communicate with relays. std::shared_ptr<client::IWebSocketClient> _client; ///< The signer used to sign Nostr events. - std::shared_ptr<ISigner> _signer; + std::shared_ptr<signer::ISigner> _signer; ///< A mutex to protect the instance properties. std::mutex _propertyMutex; @@ -324,7 +225,7 @@ private: */ void onSubscriptionMessage( std::string message, - std::function<void(const std::string&, std::shared_ptr<Event>)> eventHandler, + std::function<void(const std::string&, std::shared_ptr<data::Event>)> eventHandler, std::function<void(const std::string&)> eoseHandler, std::function<void(const std::string&, const std::string&)> closeHandler); @@ -336,15 +237,4 @@ private: */ void onAcceptance(std::string message, std::function<void(const bool)> acceptanceHandler); }; - -class ISigner -{ -public: - /** - * @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> event) = 0; -}; } // namespace nostr diff --git a/include/signer/signer.hpp b/include/signer/signer.hpp new file mode 100644 index 0000000..e68df93 --- /dev/null +++ b/include/signer/signer.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include "data/data.hpp" + +namespace nostr +{ +namespace signer +{ +/** + * @brief An interface for Nostr event signing that implements NIP-46. + */ +class ISigner +{ +public: + virtual void receiveConnection(std::string connectionToken) = 0; + + virtual void initiateConnection( + std::string relay, + std::string name, + std::string url, + std::string description) = 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<nostr::data::Event> event) = 0; +}; +} // namespace signer +} // namespace nostr diff --git a/src/client/websocketpp_client.cpp b/src/client/websocketpp_client.cpp index baae054..9967d74 100644 --- a/src/client/websocketpp_client.cpp +++ b/src/client/websocketpp_client.cpp @@ -9,6 +9,8 @@ using std::string; using std::tuple; using std::unordered_map; +namespace nostr +{ namespace client { /** @@ -127,3 +129,4 @@ private: }; }; } // namespace client +} // namespace nostr diff --git a/src/event.cpp b/src/data/event.cpp index 703efae..620ee3f 100644 --- a/src/event.cpp +++ b/src/data/event.cpp @@ -1,12 +1,12 @@ -#include <ctime> - -#include "nostr.hpp" +#include "data/data.hpp" using namespace nlohmann; using namespace std; namespace nostr { +namespace data +{ string Event::serialize() { try @@ -123,4 +123,5 @@ bool Event::operator==(const Event& other) const return this->id == other.id; }; +} // namespace data } // namespace nostr diff --git a/src/filters.cpp b/src/data/filters.cpp index 40596eb..7e1c744 100644 --- a/src/filters.cpp +++ b/src/data/filters.cpp @@ -1,10 +1,12 @@ -#include "nostr.hpp" +#include "data/data.hpp" using namespace nlohmann; using namespace std; namespace nostr { +namespace data +{ string Filters::serialize(string& subscriptionId) { try @@ -64,4 +66,5 @@ void Filters::validate() throw invalid_argument("Filters::validate: At least one filter must be set."); } }; +} // namespace data } // namespace nostr diff --git a/src/nostr_service.cpp b/src/nostr_service.cpp index 664243f..9f6b8ce 100644 --- a/src/nostr_service.cpp +++ b/src/nostr_service.cpp @@ -1,5 +1,4 @@ #include "nostr.hpp" -#include "client/web_socket_client.hpp" using namespace nlohmann; using namespace std; @@ -9,13 +8,13 @@ namespace nostr NostrService::NostrService( shared_ptr<plog::IAppender> appender, shared_ptr<client::IWebSocketClient> client, - shared_ptr<ISigner> signer) + shared_ptr<signer::ISigner> signer) : NostrService(appender, client, signer, {}) { }; NostrService::NostrService( shared_ptr<plog::IAppender> appender, shared_ptr<client::IWebSocketClient> client, - shared_ptr<ISigner> signer, + shared_ptr<signer::ISigner> signer, vector<string> relays) : _defaultRelays(relays), _client(client), _signer(signer) { @@ -58,8 +57,8 @@ vector<string> NostrService::openRelayConnections(vector<string> relays) connectionThread.join(); } - size_t targetCount = relays.size(); - size_t activeCount = this->_activeRelays.size(); + std::size_t targetCount = relays.size(); + std::size_t activeCount = this->_activeRelays.size(); PLOG_INFO << "Connected to " << activeCount << "/" << targetCount << " target relays."; // This property should only contain successful relays at this point. @@ -102,7 +101,7 @@ void NostrService::closeRelayConnections(vector<string> relays) }; // TODO: Make this method return a promise. -tuple<vector<string>, vector<string>> NostrService::publishEvent(shared_ptr<Event> event) +tuple<vector<string>, vector<string>> NostrService::publishEvent(shared_ptr<data::Event> event) { vector<string> successfulRelays; vector<string> failedRelays; @@ -174,8 +173,8 @@ tuple<vector<string>, vector<string>> NostrService::publishEvent(shared_ptr<Even } } - size_t targetCount = targetRelays.size(); - size_t successfulCount = successfulRelays.size(); + std::size_t targetCount = targetRelays.size(); + std::size_t successfulCount = successfulRelays.size(); PLOG_INFO << "Published event to " << successfulCount << "/" << targetCount << " target relays."; return make_tuple(successfulRelays, failedRelays); @@ -183,7 +182,7 @@ tuple<vector<string>, vector<string>> NostrService::publishEvent(shared_ptr<Even // TODO: Make this method return a promise. // TODO: Add a timeout to this method to prevent hanging while waiting for the relay. -vector<shared_ptr<Event>> NostrService::queryRelays(shared_ptr<Filters> filters) +vector<shared_ptr<data::Event>> NostrService::queryRelays(shared_ptr<data::Filters> filters) { if (filters->limit > 64 || filters->limit < 1) { @@ -191,7 +190,7 @@ vector<shared_ptr<Event>> NostrService::queryRelays(shared_ptr<Filters> filters) filters->limit = 16; } - vector<shared_ptr<Event>> events; + vector<shared_ptr<data::Event>> events; string subscriptionId = this->generateSubscriptionId(); string request; @@ -229,7 +228,7 @@ vector<shared_ptr<Event>> NostrService::queryRelays(shared_ptr<Filters> filters) { this->onSubscriptionMessage( payload, - [&events](const string&, shared_ptr<Event> event) + [&events](const string&, shared_ptr<data::Event> event) { events.push_back(event); }, @@ -278,8 +277,8 @@ vector<shared_ptr<Event>> NostrService::queryRelays(shared_ptr<Filters> filters) }; string NostrService::queryRelays( - shared_ptr<Filters> filters, - function<void(const string&, shared_ptr<Event>)> eventHandler, + shared_ptr<data::Filters> filters, + function<void(const string&, shared_ptr<data::Event>)> eventHandler, function<void(const string&)> eoseHandler, function<void(const string&, const string&)> closeHandler) { @@ -322,8 +321,8 @@ string NostrService::queryRelays( } } - size_t targetCount = this->_activeRelays.size(); - size_t successfulCount = successfulRelays.size(); + std::size_t targetCount = this->_activeRelays.size(); + std::size_t successfulCount = successfulRelays.size(); PLOG_INFO << "Sent query to " << successfulCount << "/" << targetCount << " open relay connections."; return subscriptionId; @@ -335,7 +334,7 @@ tuple<vector<string>, vector<string>> NostrService::closeSubscription(string sub vector<string> failedRelays; vector<string> subscriptionRelays; - size_t subscriptionRelayCount; + std::size_t subscriptionRelayCount; vector<future<tuple<string, bool>>> closeFutures; try @@ -375,7 +374,7 @@ tuple<vector<string>, vector<string>> NostrService::closeSubscription(string sub } } - size_t successfulCount = successfulRelays.size(); + std::size_t successfulCount = successfulRelays.size(); 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 @@ -596,7 +595,7 @@ bool NostrService::hasSubscription(string subscriptionId, string relay) void NostrService::onSubscriptionMessage( string message, - function<void(const string&, shared_ptr<Event>)> eventHandler, + function<void(const string&, shared_ptr<data::Event>)> eventHandler, function<void(const string&)> eoseHandler, function<void(const string&, const string&)> closeHandler) { @@ -607,8 +606,8 @@ void NostrService::onSubscriptionMessage( if (messageType == "EVENT") { string subscriptionId = jMessage.at(1); - Event event = Event::fromString(jMessage.at(2)); - eventHandler(subscriptionId, make_shared<Event>(event)); + data::Event event = data::Event::fromString(jMessage.at(2)); + eventHandler(subscriptionId, make_shared<data::Event>(event)); } else if (messageType == "EOSE") { diff --git a/src/signer/noscrypt_signer.cpp b/src/signer/noscrypt_signer.cpp new file mode 100644 index 0000000..a798271 --- /dev/null +++ b/src/signer/noscrypt_signer.cpp @@ -0,0 +1,34 @@ +#include <noscrypt.h> + +#include "signer.hpp" + +using namespace std; + +namespace nostr +{ +namespace signer +{ +class NoscryptSigner : public ISigner +{ +public: + void receiveConnection(string connectionToken) override + { + // Receive the connection token here. + }; + + void initiateConnection( + string relay, + string name, + string url, + string description) override + { + // Initiate the connection here. + }; + + void sign(shared_ptr<data::Event> event) override + { + // Sign the event here. + }; +}; +} // namespace signer +} // namespace nostr diff --git a/test/nostr_service_test.cpp b/test/nostr_service_test.cpp index b3b9b28..3e951ad 100644 --- a/test/nostr_service_test.cpp +++ b/test/nostr_service_test.cpp @@ -1,15 +1,18 @@ #include <chrono> +#include <iostream> + #include <gmock/gmock.h> #include <gtest/gtest.h> #include <nlohmann/json.hpp> #include <plog/Appenders/ConsoleAppender.h> #include <plog/Formatters/TxtFormatter.h> -#include <iostream> #include <websocketpp/client.hpp> -#include <client/web_socket_client.hpp> -#include <nostr.hpp> +#include "nostr.hpp" +#include "client/web_socket_client.hpp" +#include "signer/signer.hpp" +using namespace nostr; using namespace std; using namespace ::testing; @@ -29,13 +32,27 @@ public: MOCK_METHOD(void, closeConnection, (string uri), (override)); }; -class FakeSigner : public nostr::ISigner +class FakeSigner : public signer::ISigner { public: - void sign(shared_ptr<nostr::Event> event) override + void receiveConnection(string connectionToken) override + { + // Do nothing. + }; + + void initiateConnection( + string relay, + string name, + string url, + string description) override + { + // Do nothing. + }; + + void sign(shared_ptr<nostr::data::Event> event) override { event->sig = "fake_signature"; - } + }; }; class NostrServiceTest : public testing::Test @@ -47,9 +64,9 @@ public: "wss://nostr.thesamecat.io" }; - static const nostr::Event getTextNoteTestEvent() + static const nostr::data::Event getTextNoteTestEvent() { - nostr::Event event; + nostr::data::Event event; event.pubkey = "13tn5ccv2guflxgffq4aj0hw5x39pz70zcdrfd6vym887gry38zys28dask"; event.kind = 1; event.tags = @@ -63,12 +80,12 @@ public: return event; }; - static const vector<nostr::Event> getMultipleTextNoteTestEvents() + static const vector<nostr::data::Event> getMultipleTextNoteTestEvents() { auto now = std::chrono::system_clock::now(); std::time_t currentTime = std::chrono::system_clock::to_time_t(now); - nostr::Event event1; + nostr::data::Event event1; event1.pubkey = "13tn5ccv2guflxgffq4aj0hw5x39pz70zcdrfd6vym887gry38zys28dask"; event1.kind = 1; event1.tags = @@ -80,7 +97,7 @@ public: event1.content = "Hello, World!"; event1.createdAt = currentTime; - nostr::Event event2; + nostr::data::Event event2; event2.pubkey = "1l9d9jh67rkwayalrxcy686aujyz5pper5kzjv8jvg8pu9v9ns4ls0xvq42"; event2.kind = 1; event2.tags = @@ -92,7 +109,7 @@ public: event2.content = "Welcome to Nostr!"; event2.createdAt = currentTime; - nostr::Event event3; + nostr::data::Event event3; event3.pubkey = "187ujhtmnv82ftg03h4heetwk3dd9mlfkf8th3fvmrk20nxk9mansuzuyla"; event3.kind = 1; event3.tags = @@ -107,9 +124,9 @@ public: return { event1, event2, event3 }; }; - static const nostr::Event getLongFormTestEvent() + static const nostr::data::Event getLongFormTestEvent() { - nostr::Event event; + nostr::data::Event event; event.pubkey = "13tn5ccv2guflxgffq4aj0hw5x39pz70zcdrfd6vym887gry38zys28dask"; event.kind = 30023; event.tags = @@ -123,7 +140,7 @@ public: return event; } - static const string getTestEventMessage(shared_ptr<nostr::Event> event, string subscriptionId) + static const string getTestEventMessage(shared_ptr<nostr::data::Event> event, string subscriptionId) { auto signer = make_unique<FakeSigner>(); signer->sign(event); @@ -136,9 +153,9 @@ public: return jarr.dump(); } - static const nostr::Filters getKind0And1TestFilters() + static const nostr::data::Filters getKind0And1TestFilters() { - nostr::Filters filters; + nostr::data::Filters filters; filters.authors = { "13tn5ccv2guflxgffq4aj0hw5x39pz70zcdrfd6vym887gry38zys28dask", "1l9d9jh67rkwayalrxcy686aujyz5pper5kzjv8jvg8pu9v9ns4ls0xvq42", @@ -150,9 +167,9 @@ public: return filters; } - static const nostr::Filters getKind30023TestFilters() + static const nostr::data::Filters getKind30023TestFilters() { - nostr::Filters filters; + nostr::data::Filters filters; filters.authors = { "13tn5ccv2guflxgffq4aj0hw5x39pz70zcdrfd6vym887gry38zys28dask", "1l9d9jh67rkwayalrxcy686aujyz5pper5kzjv8jvg8pu9v9ns4ls0xvq42", @@ -461,7 +478,7 @@ TEST_F(NostrServiceTest, PublishEvent_CorrectlyIndicates_AllSuccesses) .WillRepeatedly(Invoke([](string message, string uri, function<void(const string&)> messageHandler) { json messageArr = json::parse(message); - auto event = nostr::Event::fromString(messageArr[1]); + auto event = nostr::data::Event::fromString(messageArr[1]); json jarr = json::array({ "OK", event.id, true, "Event accepted" }); messageHandler(jarr.dump()); @@ -469,7 +486,7 @@ TEST_F(NostrServiceTest, PublishEvent_CorrectlyIndicates_AllSuccesses) return make_tuple(uri, true); })); - auto testEvent = make_shared<nostr::Event>(getTextNoteTestEvent()); + auto testEvent = make_shared<nostr::data::Event>(getTextNoteTestEvent()); auto [successes, failures] = nostrService->publishEvent(testEvent); ASSERT_EQ(successes.size(), defaultTestRelays.size()); @@ -515,7 +532,7 @@ TEST_F(NostrServiceTest, PublishEvent_CorrectlyIndicates_AllFailures) return make_tuple(uri, false); })); - auto testEvent = make_shared<nostr::Event>(getTextNoteTestEvent()); + auto testEvent = make_shared<nostr::data::Event>(getTextNoteTestEvent()); auto [successes, failures] = nostrService->publishEvent(testEvent); ASSERT_EQ(successes.size(), 0); @@ -566,7 +583,7 @@ TEST_F(NostrServiceTest, PublishEvent_CorrectlyIndicates_MixedSuccessesAndFailur .WillRepeatedly(Invoke([](string message, string uri, function<void(const string&)> messageHandler) { json messageArr = json::parse(message); - auto event = nostr::Event::fromString(messageArr[1]); + auto event = nostr::data::Event::fromString(messageArr[1]); json jarr = json::array({ "OK", event.id, true, "Event accepted" }); messageHandler(jarr.dump()); @@ -574,7 +591,7 @@ TEST_F(NostrServiceTest, PublishEvent_CorrectlyIndicates_MixedSuccessesAndFailur return make_tuple(uri, true); })); - auto testEvent = make_shared<nostr::Event>(getTextNoteTestEvent()); + auto testEvent = make_shared<nostr::data::Event>(getTextNoteTestEvent()); auto [successes, failures] = nostrService->publishEvent(testEvent); ASSERT_EQ(successes.size(), 1); @@ -616,7 +633,7 @@ TEST_F(NostrServiceTest, PublishEvent_CorrectlyIndicates_RejectedEvent) .WillRepeatedly(Invoke([](string message, string uri, function<void(const string&)> messageHandler) { json messageArr = json::parse(message); - auto event = nostr::Event::fromString(messageArr[1]); + auto event = nostr::data::Event::fromString(messageArr[1]); json jarr = json::array({ "OK", event.id, false, "Event rejected" }); messageHandler(jarr.dump()); @@ -624,7 +641,7 @@ TEST_F(NostrServiceTest, PublishEvent_CorrectlyIndicates_RejectedEvent) return make_tuple(uri, true); })); - auto testEvent = make_shared<nostr::Event>(getTextNoteTestEvent()); + auto testEvent = make_shared<nostr::data::Event>(getTextNoteTestEvent()); auto [successes, failures] = nostrService->publishEvent(testEvent); ASSERT_EQ(failures.size(), defaultTestRelays.size()); @@ -667,7 +684,7 @@ TEST_F(NostrServiceTest, PublishEvent_CorrectlyIndicates_EventRejectedBySomeRela .WillRepeatedly(Invoke([](string message, string uri, function<void(const string&)> messageHandler) { json messageArr = json::parse(message); - auto event = nostr::Event::fromString(messageArr[1]); + auto event = nostr::data::Event::fromString(messageArr[1]); json jarr = json::array({ "OK", event.id, true, "Event accepted" }); messageHandler(jarr.dump()); @@ -679,7 +696,7 @@ TEST_F(NostrServiceTest, PublishEvent_CorrectlyIndicates_EventRejectedBySomeRela .WillRepeatedly(Invoke([](string message, string uri, function<void(const string&)> messageHandler) { json messageArr = json::parse(message); - auto event = nostr::Event::fromString(messageArr[1]); + auto event = nostr::data::Event::fromString(messageArr[1]); json jarr = json::array({ "OK", event.id, false, "Event rejected" }); messageHandler(jarr.dump()); @@ -687,7 +704,7 @@ TEST_F(NostrServiceTest, PublishEvent_CorrectlyIndicates_EventRejectedBySomeRela return make_tuple(uri, true); })); - auto testEvent = make_shared<nostr::Event>(getTextNoteTestEvent()); + auto testEvent = make_shared<nostr::data::Event>(getTextNoteTestEvent()); auto [successes, failures] = nostrService->publishEvent(testEvent); ASSERT_EQ(successes.size(), 1); @@ -725,16 +742,16 @@ TEST_F(NostrServiceTest, QueryRelays_ReturnsEvents_UpToEOSE) nostrService->openRelayConnections(); auto testEvents = getMultipleTextNoteTestEvents(); - vector<shared_ptr<nostr::Event>> signedTestEvents; - for (nostr::Event testEvent : testEvents) + vector<shared_ptr<nostr::data::Event>> signedTestEvents; + for (nostr::data::Event testEvent : testEvents) { - auto signedEvent = make_shared<nostr::Event>(testEvent); + auto signedEvent = make_shared<nostr::data::Event>(testEvent); signer->sign(signedEvent); auto serializedEvent = signedEvent->serialize(); - auto deserializedEvent = nostr::Event::fromString(serializedEvent); + auto deserializedEvent = nostr::data::Event::fromString(serializedEvent); - signedEvent = make_shared<nostr::Event>(deserializedEvent); + signedEvent = make_shared<nostr::data::Event>(deserializedEvent); signedTestEvents.push_back(signedEvent); } @@ -751,7 +768,7 @@ TEST_F(NostrServiceTest, QueryRelays_ReturnsEvents_UpToEOSE) for (auto event : testEvents) { - auto sendableEvent = make_shared<nostr::Event>(event); + auto sendableEvent = make_shared<nostr::data::Event>(event); signer->sign(sendableEvent); json jarr = json::array({ "EVENT", subscriptionId, sendableEvent->serialize() }); messageHandler(jarr.dump()); @@ -770,7 +787,7 @@ TEST_F(NostrServiceTest, QueryRelays_ReturnsEvents_UpToEOSE) return make_tuple(uri, true); })); - auto filters = make_shared<nostr::Filters>(getKind0And1TestFilters()); + auto filters = make_shared<nostr::data::Filters>(getKind0And1TestFilters()); auto results = nostrService->queryRelays(filters); // TODO: Check results size when the queryRelays method deduplicates results before returning. @@ -783,7 +800,7 @@ TEST_F(NostrServiceTest, QueryRelays_ReturnsEvents_UpToEOSE) find_if( signedTestEvents.begin(), signedTestEvents.end(), - [&resultEvent](shared_ptr<nostr::Event> testEvent) + [&resultEvent](shared_ptr<nostr::data::Event> testEvent) { return *testEvent == *resultEvent; }), @@ -822,16 +839,16 @@ TEST_F(NostrServiceTest, QueryRelays_CallsHandler_WithReturnedEvents) nostrService->openRelayConnections(); auto testEvents = getMultipleTextNoteTestEvents(); - vector<shared_ptr<nostr::Event>> signedTestEvents; - for (nostr::Event testEvent : testEvents) + vector<shared_ptr<nostr::data::Event>> signedTestEvents; + for (nostr::data::Event testEvent : testEvents) { - auto signedEvent = make_shared<nostr::Event>(testEvent); + auto signedEvent = make_shared<nostr::data::Event>(testEvent); signer->sign(signedEvent); auto serializedEvent = signedEvent->serialize(); - auto deserializedEvent = nostr::Event::fromString(serializedEvent); + auto deserializedEvent = nostr::data::Event::fromString(serializedEvent); - signedEvent = make_shared<nostr::Event>(deserializedEvent); + signedEvent = make_shared<nostr::data::Event>(deserializedEvent); signedTestEvents.push_back(signedEvent); } @@ -847,7 +864,7 @@ TEST_F(NostrServiceTest, QueryRelays_CallsHandler_WithReturnedEvents) for (auto event : testEvents) { - auto sendableEvent = make_shared<nostr::Event>(event); + auto sendableEvent = make_shared<nostr::data::Event>(event); signer->sign(sendableEvent); json jarr = json::array({ "EVENT", subscriptionId, sendableEvent->serialize() }); messageHandler(jarr.dump()); @@ -859,21 +876,21 @@ TEST_F(NostrServiceTest, QueryRelays_CallsHandler_WithReturnedEvents) return make_tuple(uri, true); })); - auto filters = make_shared<nostr::Filters>(getKind0And1TestFilters()); + auto filters = make_shared<nostr::data::Filters>(getKind0And1TestFilters()); promise<void> eoseReceivedPromise; auto eoseReceivedFuture = eoseReceivedPromise.get_future(); int eoseCount = 0; string generatedSubscriptionId = nostrService->queryRelays( filters, - [&generatedSubscriptionId, &signedTestEvents](const string& subscriptionId, shared_ptr<nostr::Event> event) + [&generatedSubscriptionId, &signedTestEvents](const string& subscriptionId, shared_ptr<nostr::data::Event> event) { ASSERT_STREQ(subscriptionId.c_str(), generatedSubscriptionId.c_str()); ASSERT_NE( find_if( signedTestEvents.begin(), signedTestEvents.end(), - [&event](shared_ptr<nostr::Event> testEvent) + [&event](shared_ptr<nostr::data::Event> testEvent) { return *testEvent == *event; }), @@ -944,16 +961,16 @@ TEST_F(NostrServiceTest, Service_MaintainsMultipleSubscriptions_ThenClosesAll) // Mock relay responses. auto testEvents = getMultipleTextNoteTestEvents(); - vector<shared_ptr<nostr::Event>> signedTestEvents; - for (nostr::Event testEvent : testEvents) + vector<shared_ptr<nostr::data::Event>> signedTestEvents; + for (nostr::data::Event testEvent : testEvents) { - auto signedEvent = make_shared<nostr::Event>(testEvent); + auto signedEvent = make_shared<nostr::data::Event>(testEvent); signer->sign(signedEvent); auto serializedEvent = signedEvent->serialize(); - auto deserializedEvent = nostr::Event::fromString(serializedEvent); + auto deserializedEvent = nostr::data::Event::fromString(serializedEvent); - signedEvent = make_shared<nostr::Event>(deserializedEvent); + signedEvent = make_shared<nostr::data::Event>(deserializedEvent); signedTestEvents.push_back(signedEvent); } @@ -970,7 +987,7 @@ TEST_F(NostrServiceTest, Service_MaintainsMultipleSubscriptions_ThenClosesAll) for (auto event : testEvents) { - auto sendableEvent = make_shared<nostr::Event>(event); + auto sendableEvent = make_shared<nostr::data::Event>(event); signer->sign(sendableEvent); json jarr = json::array({ "EVENT", subscriptionIds.at(0), sendableEvent->serialize() }); messageHandler(jarr.dump()); @@ -991,7 +1008,7 @@ TEST_F(NostrServiceTest, Service_MaintainsMultipleSubscriptions_ThenClosesAll) for (auto event : testEvents) { - auto sendableEvent = make_shared<nostr::Event>(event); + auto sendableEvent = make_shared<nostr::data::Event>(event); signer->sign(sendableEvent); json jarr = json::array({ "EVENT", subscriptionIds.at(1), sendableEvent->serialize() }); messageHandler(jarr.dump()); @@ -1004,8 +1021,8 @@ TEST_F(NostrServiceTest, Service_MaintainsMultipleSubscriptions_ThenClosesAll) })); // Send queries. - auto shortFormFilters = make_shared<nostr::Filters>(getKind0And1TestFilters()); - auto longFormFilters = make_shared<nostr::Filters>(getKind30023TestFilters()); + auto shortFormFilters = make_shared<nostr::data::Filters>(getKind0And1TestFilters()); + auto longFormFilters = make_shared<nostr::data::Filters>(getKind30023TestFilters()); promise<void> shortFormPromise; promise<void> longFormPromise; auto shortFormFuture = shortFormPromise.get_future(); @@ -1013,14 +1030,14 @@ TEST_F(NostrServiceTest, Service_MaintainsMultipleSubscriptions_ThenClosesAll) string shortFormSubscriptionId = nostrService->queryRelays( shortFormFilters, - [&shortFormSubscriptionId, &signedTestEvents](const string& subscriptionId, shared_ptr<nostr::Event> event) + [&shortFormSubscriptionId, &signedTestEvents](const string& subscriptionId, shared_ptr<nostr::data::Event> event) { ASSERT_STREQ(subscriptionId.c_str(), shortFormSubscriptionId.c_str()); ASSERT_NE( find_if( signedTestEvents.begin(), signedTestEvents.end(), - [&event](shared_ptr<nostr::Event> testEvent) + [&event](shared_ptr<nostr::data::Event> testEvent) { return *testEvent == *event; }), @@ -1035,14 +1052,14 @@ TEST_F(NostrServiceTest, Service_MaintainsMultipleSubscriptions_ThenClosesAll) [](const string&, const string&) {}); string longFormSubscriptionId = nostrService->queryRelays( shortFormFilters, - [&longFormSubscriptionId, &signedTestEvents](const string& subscriptionId, shared_ptr<nostr::Event> event) + [&longFormSubscriptionId, &signedTestEvents](const string& subscriptionId, shared_ptr<nostr::data::Event> event) { ASSERT_STREQ(subscriptionId.c_str(), longFormSubscriptionId.c_str()); ASSERT_NE( find_if( signedTestEvents.begin(), signedTestEvents.end(), - [&event](shared_ptr<nostr::Event> testEvent) + [&event](shared_ptr<nostr::data::Event> testEvent) { return *testEvent == *event; }), |